gem selector sketch

This commit is contained in:
Vinicius Moreira
2019-09-11 16:00:02 -03:00
parent 696b6169ac
commit ba5e1da11b
79 changed files with 4117 additions and 112 deletions

0
bauh/api/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,50 @@
from abc import ABC, abstractmethod
from typing import Set
class MemoryCache(ABC):
"""
Represents a memory cache.
"""
@abstractmethod
def is_enabled(self):
pass
@abstractmethod
def add(self, key: str, val: object):
pass
@abstractmethod
def add_non_existing(self, key: str, val: object):
pass
@abstractmethod
def get(self, key: str):
pass
@abstractmethod
def delete(self, key):
pass
@abstractmethod
def keys(self) -> Set[str]:
pass
@abstractmethod
def clean_expired(self):
pass
class MemoryCacheFactory(ABC):
"""
Instantiate new memory cache instances.
"""
@abstractmethod
def new(self, expiration: int) -> MemoryCache:
"""
:param expiration: expiration time for the cache keys in seconds. Use -1 to disable this feature.
:return:
"""
pass

View File

@@ -0,0 +1,30 @@
import logging
from bauh.api.abstract.cache import MemoryCacheFactory
from bauh.api.abstract.disk import DiskCacheLoaderFactory
from bauh.api.http import HttpClient
class ApplicationContext:
def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: dict,
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
logger: logging.Logger):
"""
:param disk_cache: if package data should be cached to disk
:param download_icons: if packages icons should be downloaded
:param http_client: a shared instance of http client
:param app_root_dir: GUI root dir
:param i18n: the i18n dictionary keys
:param cache_factory:
:param disk_loader_factory:
:param logger: a logger instance
"""
self.disk_cache = disk_cache
self.download_icons = download_icons
self.http_client = http_client
self.app_root_dir = app_root_dir
self.i18n = i18n
self.cache_factory = cache_factory
self.disk_loader_factory = disk_loader_factory
self.logger = logger

View File

@@ -0,0 +1,214 @@
import json
import os
import shutil
from abc import ABC, abstractmethod
from pathlib import Path
from typing import List, Set, Type
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction
class SearchResult:
def __init__(self, installed: List[SoftwarePackage], new: List[SoftwarePackage], total: int):
"""
:param installed: already installed packages
:param new: new packages found
:param total: total number of applications actually found
"""
self.installed = installed
self.new = new
self.total = total
class SoftwareManager(ABC):
"""
Base controller class that will be called by the graphical interface to execute operations.
"""
def __init__(self, context: ApplicationContext):
"""
:param context:
"""
self.context = context
@abstractmethod
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int) -> SearchResult:
"""
:param words: the words typed by the user
:param disk_loader: a running disk loader thread that loads package data from the disk asynchronously
:param limit: the max number of packages to be retrieved. <= 1 should retrieve everything
:return:
"""
pass
@abstractmethod
def read_installed(self, disk_loader: DiskCacheLoader, limit: int, only_apps: bool, pkg_types: Set[Type[SoftwarePackage]]) -> SearchResult:
"""
:param disk_loader: a running disk loader thread that loads application data from the disk asynchronously
:param limit: the max number of packages to be retrieved. <= 1 should retrieve everything
:param only_apps: if only application packages should be retrieved
:param pkg_types: use 'None' to bring any or specify some
:return:
"""
pass
@abstractmethod
def downgrade(self, pkg: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool:
"""
downgrades a package version
:param pkg:
:param root_password: the root user password (if required)
:param handler: a subprocess handler
:return:
"""
pass
def clean_cache_for(self, pkg: SoftwarePackage):
"""
Cleans cached package cached data. This default implementation only cleans the cached data from the heard disk
:param pkg:
:return:
"""
if pkg.supports_disk_cache() and os.path.exists(pkg.get_disk_cache_path()):
shutil.rmtree(pkg.get_disk_cache_path())
@abstractmethod
def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
"""
:param pkg:
:param root_password: the root user password (if required)
:param watcher:
:return:
"""
pass
@abstractmethod
def uninstall(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
"""
:param pkg:
:param root_password: the root user password (if required)
:param watcher:
:return: if the uninstall succeeded
"""
pass
@abstractmethod
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
"""
:return: the managed package class type
"""
pass
@abstractmethod
def get_info(self, pkg: SoftwarePackage) -> dict:
"""
retrieve the package information
:param pkg:
:return: a dictionary with the attributes to be shown
"""
pass
@abstractmethod
def get_history(self, pkg: SoftwarePackage) -> PackageHistory:
"""
:param pkg:
:return:
"""
pass
@abstractmethod
def install(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
"""
:param pkg:
:param root_password: the root user password (if required)
:param watcher:
:return: if the installation succeeded
"""
pass
@abstractmethod
def is_enabled(self) -> bool:
"""
:return: if the instance is available to perform actions
"""
pass
def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
"""
Saves the package data to the hard disk.
:param pkg:
:param icon_bytes:
:param only_icon: if only the icon should be saved
:return:
"""
if pkg.supports_disk_cache():
if not only_icon:
Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
data = pkg.get_data_to_cache()
if data:
with open(pkg.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(data))
if icon_bytes:
Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
with open(pkg.get_disk_icon_path(), 'wb+') as f:
f.write(icon_bytes)
@abstractmethod
def requires_root(self, action: str, pkg: SoftwarePackage):
"""
if a given action requires root privileges to be executed. Current actions are: 'install', 'uninstall', 'downgrade', 'search', 'refresh'
:param action:
:param pkg:
:return:
"""
pass
@abstractmethod
def prepare(self):
"""
It prepares the manager to start working. It will be called by GUI. Do not call it within.
:return:
"""
pass
@abstractmethod
def list_updates(self) -> List[PackageUpdate]:
"""
:return: available package updates
"""
pass
@abstractmethod
def list_warnings(self) -> List[str]:
"""
:return: a list of warnings to be shown to the user
"""
pass
@abstractmethod
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
"""
:param limit: max suggestions to be returned. If limit < 0, it should not be considered
:return: a list of package suggestions
"""
pass
def execute_custom_action(self, action: PackageAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
"""
At the moment the GUI implements this action. No need to implement it yourself.
:param action:
:param pkg:
:param root_password:
:param watcher:
:return: if the action resulted in success
"""
pass

42
bauh/api/abstract/disk.py Normal file
View File

@@ -0,0 +1,42 @@
from abc import ABC, abstractmethod
from typing import Type
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.model import SoftwarePackage
class DiskCacheLoader:
"""
Reads cached data from the disk and fills package instances.
"""
def map(self, cache: MemoryCache, pkg_type: Type[SoftwarePackage]):
"""
maps a given cache instance for a given package type
:param cache:
:param pkg_type:
:return:
"""
pass
def fill(self, pkg: SoftwarePackage):
"""
fill cached data from the disk of a given package instance
If a cache mapping was previously done, then data retrieved will be cached to memory as well.
:param pkg:
:return:
"""
pass
class DiskCacheLoaderFactory(ABC):
@abstractmethod
def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache):
"""
Associated a cache instance to instances of a given SoftwarePackage class
:param pkg_type:
:param cache:
:return:
"""
pass

View File

@@ -0,0 +1,67 @@
from typing import List
from bauh.api.abstract.view import MessageType, ViewComponent
class ProcessWatcher:
"""
Represents an view component watching background processes. It's a bridge for ApplicationManager instances notifiy the view of processes progression and also
request any user interaction without the need of knowing any GUI code.
"""
def print(self, msg: str):
"""
prints a given message to the user. In the current GUI implementation, the message is printed on the terminal window ("Details")
:param msg:
:return:
"""
pass
def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None)-> bool:
"""
request a user confirmation. In the current GUI implementation, it shows a popup to the user.
:param title: popup title
:param body: popup body
:param components: extra view components that will be rendered to the confirmation popup.
:param confirmation_label: optional confirmation button label (default to 'yes')
:param deny_label: optional deny button label (default to 'no')
:return: if the request was confirmed by the user
"""
pass
def show_message(self, title: str, body: str, type_: MessageType = MessageType.INFO):
"""
shows a message to the user. In the current GUI implementation, it shows a popup.
:param title:
:param body:
:param type_: determines the icon that will be displayed
:return:
"""
pass
def change_status(self, msg: str):
"""
Changes the process status. In the current GUI implementation, the process status is displayed above the toolbar.
:param msg: msg
:return:
"""
pass
def change_substatus(self, msg: str):
"""
Changes the process substatus. In the current GUI implementation, the process substatus is displayed above the progress bar.
:param msg:
:return:
"""
def change_progress(self, val: int):
"""
Changes the process progress. In the current GUI implementation, the progress is displayed as a bottom bar.
:param val: a val between 0 and 100
:return:
"""
def should_stop(self) -> bool:
"""
:return: if the use requested to stop the process.
"""

222
bauh/api/abstract/model.py Normal file
View File

@@ -0,0 +1,222 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import List
from bauh.api.constants import CACHE_PATH
class PackageAction:
def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, requires_root: bool):
"""
:param i18_label_key: the i18n key that will be used to display the action name
:param i18n_status_key: the i18n key that will be used to display the action name being executed
:param icon_path: the action icon path. Use None for no icon
:param manager_method: the SoftwareManager method name that should be called. The method must has the following parameters: (pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher)
:param requires_root:
"""
self.id = id
self.i18_label_key = i18_label_key
self.i18n_status_key = i18n_status_key
self.icon_path = icon_path
self.manager_method = manager_method
self.requires_root = requires_root
class PackageStatus(Enum):
READY = 1 # when all package data is already filled
LOADING_DATA = 2 # when some package data is already being retrieved asynchronously
class SoftwarePackage(ABC):
def __init__(self, id: str = None, version: str = None, name: str = None, description: str = None, latest_version: str = None,
icon_url: str = None, status: PackageStatus = PackageStatus.READY, installed: bool = False, update: bool = False,
size: int = None, categories: List[str] = None):
"""
:param id:
:param version:
:param name:
:param description:
:param latest_version:
:param icon_url: the path to the package icon. It can be an URL or PATH
:param status:
:param installed:
:param update: if there is an update for package
:param size: package size in BYTES
:param categories: package categories. i.e: video editor, web browser, etc
"""
self.id = id
self.name = name
self.description = description
self.status = status
self.version = version
self.latest_version = latest_version
self.icon_url = icon_url
self.installed = installed
self.update = update
self.size = size
self.categories = categories
@abstractmethod
def has_history(self):
"""
:return: if the application has a commit history that can be shown to the user
"""
pass
@abstractmethod
def has_info(self):
"""
:return: if the application has additional information that can be shown to the user
"""
pass
@abstractmethod
def can_be_downgraded(self):
pass
def can_be_uninstalled(self):
return self.installed
def can_be_installed(self):
return not self.installed
@abstractmethod
def get_type(self):
"""
:return: a string that represents the application type
"""
pass
@abstractmethod
def get_default_icon_path(self):
"""
:return: the path of a default icon when the application icon could not be retrieved (or will not be retrieved)
"""
pass
@abstractmethod
def get_type_icon_path(self):
"""
:return: the path of the application type icon
"""
@abstractmethod
def is_application(self):
"""
:return: if the package is an application
"""
pass
def supports_disk_cache(self):
"""
:return: if some application data and icon should be cached to the user disk
"""
return self.installed and self.is_application()
def get_disk_cache_path(self):
"""
:return: base cache path for the specific app type
"""
return CACHE_PATH + '/' + self.get_type()
def get_disk_icon_path(self):
return '{}/icon.png'.format(self.get_disk_cache_path())
def get_disk_data_path(self):
return '{}/data.json'.format(self.get_disk_cache_path())
@abstractmethod
def get_data_to_cache(self) -> dict:
"""
:return: the application data that should be cached in disk / memory for quick access
"""
pass
@abstractmethod
def fill_cached_data(self, data: dict):
"""
sets cached data to the current instance
:param data:
:return:
"""
pass
@abstractmethod
def can_be_run(self) -> str:
"""
:return: whether the app can be run via the GUI
"""
@abstractmethod
def get_command(self) -> str:
"""
:return: the command to run the application
"""
pass
def is_trustable(self) -> bool:
"""
:return: if the package is distributed by a trustable source
"""
return False
@abstractmethod
def get_publisher(self) -> str:
"""
:return: the package publisher / maintainer
"""
pass
def get_custom_supported_actions(self) -> List[PackageAction]:
"""
:return: custom supported actions
"""
pass
def __str__(self):
return '{} (id={}, name={})'.format(self.__class__.__name__, self.id, self.name)
class PackageUpdate:
def __init__(self, pkg_id: str, version: str, pkg_type: str):
"""
:param pkg_id: an unique package identifier
:param version: the new version
:param pkg_type: the package type
"""
self.id = pkg_id
self.version = version
self.type = pkg_type
def __str__(self):
return '{} (id={}, type={}, new_version={})'.format(self.__class__.__name__, self.id, self.type, self.type)
class PackageHistory:
def __init__(self, pkg: SoftwarePackage, history: List[dict], pkg_status_idx: int):
"""
:param pkg
:param history: a list with the package history.
:param pkg_status_idx: 'history' index in which the application is current found
"""
self.pkg = pkg
self.history = history
self.pkg_status_idx = pkg_status_idx
class SuggestionPriority(Enum):
LOW = 0
MEDIUM = 1
HIGH = 2
TOP = 3
class PackageSuggestion:
def __init__(self, package: SoftwarePackage, priority: SuggestionPriority):
self.package = package
self.priority = priority

90
bauh/api/abstract/view.py Normal file
View File

@@ -0,0 +1,90 @@
from abc import ABC
from enum import Enum
from typing import List, Set
class MessageType:
INFO = 0
WARNING = 1
ERROR = 2
class ViewComponent(ABC):
"""
Represents a GUI component
"""
def __init__(self, id_: str):
self.id = id_
class InputViewComponent(ViewComponent):
"""
Represents an component which needs a user interaction to provide its value
"""
class InputOption:
"""
Represents a select component option.
"""
def __init__(self, label: str, value: str, tooltip: str = None, icon_path: str = None):
"""
:param label: the string that will be shown to the user
:param value: the option value (not shown)
:param tooltip: an optional tooltip
:param icon_path: an optional icon path
"""
if not label:
raise Exception("'label' must be a not blank string")
if not value:
raise Exception("'value' must be a not blank string")
self.label = label
self.value = value
self.tooltip = tooltip
self.icon_path = icon_path
def __hash__(self):
return hash(self.label) + hash(self.value)
class SelectViewType(Enum):
RADIO = 0
COMBO = 1
class SingleSelectComponent(InputViewComponent):
def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None, max_per_line: int = 1, id_: str = None):
super(SingleSelectComponent, self).__init__(id_=id_)
if options is None or len(options) < 2:
raise Exception("'options' must be a list with at least 2 elements")
self.type = type_
self.label = label
self.options = options
self.value = default_option
self.max_per_line = max_per_line
class MultipleSelectComponent(InputViewComponent):
def __init__(self, label: str, options: List[InputOption], default_options: Set[InputOption] = None, max_per_line: int = 1, id_: str = None):
super(MultipleSelectComponent, self).__init__(id_=id_)
if not options:
raise Exception("'options' cannot be None or empty")
self.options = options
self.label = label
self.values = default_options if default_options else set()
self.max_per_line = max_per_line
class TextComponent(ViewComponent):
def __init__(self, html: str, id_: str = None):
super(TextComponent, self).__init__(id_=id_)
self.value = html

4
bauh/api/constants.py Normal file
View File

@@ -0,0 +1,4 @@
from pathlib import Path
HOME_PATH = Path.home()
CACHE_PATH = '{}/.cache/bauh'.format(HOME_PATH)

3
bauh/api/exception.py Normal file
View File

@@ -0,0 +1,3 @@
class NoInternetException(Exception):
pass

40
bauh/api/http.py Normal file
View File

@@ -0,0 +1,40 @@
import logging
import time
import traceback
import requests
class HttpClient:
def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = 30, sleep: float = 0.5):
self.max_attempts = max_attempts
self.session = requests.Session()
self.timeout = timeout
self.sleep = sleep
self.logger = logger
def get(self, url: str):
cur_attempts = 1
while cur_attempts <= self.max_attempts:
cur_attempts += 1
try:
res = self.session.get(url, timeout=self.timeout)
if res.status_code == 200:
return res
if self.sleep > 0:
time.sleep(self.sleep)
except:
self.logger.error("Could not retrieve data from '{}'".format(url))
traceback.print_exc()
continue
self.logger.warning("Could not retrieve data from '{}'".format(url))
def get_json(self, url: str):
res = self.get(url)
return res.json() if res else None

View File

@@ -2,15 +2,16 @@ import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication
from bauh_api.abstract.controller import ApplicationContext
from bauh_api.http import HttpClient
from bauh import __version__, __app_name__, app_args, ROOT_DIR
from bauh.core import extensions
from bauh.api.abstract.controller import ApplicationContext
from bauh.api.http import HttpClient
from bauh.core import gems, config
from bauh.core.controller import GenericSoftwareManager
from bauh.util import util, logs, resource
from bauh.util.cache import DefaultMemoryCacheFactory, CacheCleaner
from bauh.util.disk import DefaultDiskCacheLoaderFactory
from bauh.view.qt.gem_selector import GemSelectorPanel
from bauh.view.qt.systray import TrayIcon
from bauh.view.qt.window import ManageWindow
@@ -33,37 +34,48 @@ context = ApplicationContext(i18n=i18n,
disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=args.disk_cache),
logger=logger)
managers = extensions.load_managers(context=context, locale=args.locale)
manager = GenericSoftwareManager(managers, context=context, app_args=args)
manager.prepare()
app = QApplication(sys.argv)
app.setApplicationName(__app_name__)
app.setApplicationVersion(__version__)
app.setWindowIcon(QIcon(resource.get_path('img/logo.svg')))
manage_window = ManageWindow(i18n=i18n,
manager=manager,
icon_cache=icon_cache,
disk_cache=args.disk_cache,
download_icons=bool(args.download_icons),
screen_size=app.primaryScreen().size(),
suggestions=args.sugs,
display_limit=args.max_displayed)
user_config = config.read()
if args.tray:
trayIcon = TrayIcon(locale_keys=i18n,
manager=manager,
manage_window=manage_window,
check_interval=args.check_interval,
update_notification=bool(args.update_notification))
manage_window.tray_icon = trayIcon
trayIcon.show()
if not user_config.gems:
managers = gems.load_managers(context=context, locale=args.locale)
else:
manage_window.refresh_apps()
manage_window.show()
managers = gems.load_managers(context=context, locale=args.locale, names=user_config.gems)
enabled_managers = [m for m in managers if m.is_enabled()]
if not user_config.gems and enabled_managers:
gem_panel = GemSelectorPanel(enabled_managers, i18n, boot=True)
gem_panel.show()
else:
manager = GenericSoftwareManager(enabled_managers, context=context, app_args=args)
manager.prepare()
manage_window = ManageWindow(i18n=i18n,
manager=manager,
icon_cache=icon_cache,
disk_cache=args.disk_cache,
download_icons=bool(args.download_icons),
screen_size=app.primaryScreen().size(),
suggestions=args.sugs,
display_limit=args.max_displayed)
if args.tray:
trayIcon = TrayIcon(locale_keys=i18n,
manager=manager,
manage_window=manage_window,
check_interval=args.check_interval,
update_notification=bool(args.update_notification))
manage_window.tray_icon = trayIcon
trayIcon.show()
else:
manage_window.refresh_apps()
manage_window.show()
cache_cleaner.start()
cache_cleaner.start()
sys.exit(app.exec_())

0
bauh/commons/__init__.py Normal file
View File

11
bauh/commons/html.py Normal file
View File

@@ -0,0 +1,11 @@
import re
HTML_RE = re.compile(r'<[^>]+>')
def strip_html(string: str):
return HTML_RE.sub('', string)
def bold(text: str) -> str:
return '<span style="font-weight: bold">{}</span>'.format(text)

3
bauh/commons/resource.py Normal file
View File

@@ -0,0 +1,3 @@
def get_path(resource_path, root_dir: str):
return root_dir + '/resources/' + resource_path

135
bauh/commons/system.py Normal file
View File

@@ -0,0 +1,135 @@
import os
import subprocess
from subprocess import PIPE
from typing import List
# default environment variables for subprocesses.
from bauh.api.abstract.handler import ProcessWatcher
GLOBAL_INTERPRETER_PATH = ':'.join(os.getenv('PATH').split(':')[1:])
USE_GLOBAL_INTERPRETER = bool(os.getenv('VIRTUAL_ENV'))
def gen_env(global_interpreter: bool) -> dict:
res = {'LANG': 'en'}
if global_interpreter: # to avoid subprocess calls to the virtualenv python interpreter instead of the global one.
res['PATH'] = GLOBAL_INTERPRETER_PATH
return res
class SystemProcess:
"""
Represents a system process being executed.
"""
def __init__(self, subproc: subprocess.Popen, success_phrase: str = None, wrong_error_phrase: str = '[sudo] password for', check_error_output: bool = True):
self.subproc = subproc
self.success_phrase = success_phrase
self.wrong_error_phrase = wrong_error_phrase
self.check_error_output = check_error_output
def wait(self):
self.subproc.wait()
class ProcessHandler:
"""
It handles a process execution and notifies a specified watcher.
"""
def __init__(self, watcher: ProcessWatcher = None):
self.watcher = watcher
def _notify_watcher(self, msg: str):
if self.watcher:
self.watcher.print(msg)
def handle(self, process: SystemProcess) -> bool:
self._notify_watcher(' '.join(process.subproc.args) + '\n')
already_succeeded = False
for output in process.subproc.stdout:
line = output.decode().strip()
if line:
self._notify_watcher(line)
if process.success_phrase and process.success_phrase in line:
already_succeeded = True
if already_succeeded:
return True
for output in process.subproc.stderr:
line = output.decode().strip()
if line:
self._notify_watcher(line)
if process.check_error_output:
if process.wrong_error_phrase and process.wrong_error_phrase in line:
continue
else:
break
return process.subproc.returncode is None or process.subproc.returncode == 0
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True,
cwd: str = '.', global_interpreter: bool = USE_GLOBAL_INTERPRETER) -> str:
"""
runs a given command and returns its default output
:param cmd:
:param expected_code:
:param ignore_return_code:
:param print_error:
:param global_interpreter
:return:
"""
args = {
"shell": True,
"stdout": PIPE,
"env": gen_env(global_interpreter),
'cwd': cwd
}
if not print_error:
args["stderr"] = subprocess.DEVNULL
res = subprocess.run(cmd, **args)
return res.stdout.decode() if ignore_return_code or res.returncode == expected_code else None
def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin = None,
global_interpreter: bool = USE_GLOBAL_INTERPRETER) -> subprocess.Popen:
args = {
"stdout": PIPE,
"stderr": PIPE,
"cwd": cwd,
"shell": shell,
"env": gen_env(global_interpreter)
}
if input:
args['stdin'] = stdin
return subprocess.Popen(cmd, **args)
def new_root_subprocess(cmd: List[str], root_password: str, cwd: str = '.',
global_interpreter: bool = USE_GLOBAL_INTERPRETER) -> subprocess.Popen:
pwdin, final_cmd = None, []
if root_password is not None:
final_cmd.extend(['sudo', '-S'])
pwdin = new_subprocess(['echo', root_password], global_interpreter=global_interpreter).stdout
final_cmd.extend(cmd)
return subprocess.Popen(final_cmd, stdin=pwdin, stdout=PIPE, stderr=PIPE, cwd=cwd)
def notify_user(msg: str, app_name: str, icon_path: str):
os.system("notify-send -a {} {} '{}'".format(app_name, "-i {}".format(icon_path) if icon_path else '', msg))

33
bauh/core/config.py Normal file
View File

@@ -0,0 +1,33 @@
import json
import os
from pathlib import Path
from typing import List
from bauh import __app_name__
from bauh.api.constants import HOME_PATH
CONFIG_PATH = '{}/.config/{}'.format(HOME_PATH, __app_name__)
FILE_PATH = '{}/config.json'.format(CONFIG_PATH)
class Configuration:
def __init__(self, gems: List[str]):
self.gems = gems
def read() -> Configuration:
if os.path.exists(FILE_PATH):
with open(FILE_PATH) as f:
config_file = f.read()
return Configuration(**json.loads(config_file))
return Configuration(gems=None)
def save(config: Configuration):
Path(CONFIG_PATH).mkdir(parents=True, exist_ok=True)
with open(FILE_PATH, 'w+') as f:
f.write(json.dumps(config.__dict__, indent=2))

View File

@@ -2,10 +2,10 @@ from argparse import Namespace
from threading import Thread
from typing import List, Set, Type
from bauh_api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
from bauh_api.abstract.disk import DiskCacheLoader
from bauh_api.abstract.handler import ProcessWatcher
from bauh_api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction
SUGGESTIONS_LIMIT = 5

View File

@@ -1,44 +0,0 @@
import inspect
import os
import pkgutil
from typing import List
from bauh_api.abstract.controller import SoftwareManager, ApplicationContext
from bauh import __app_name__
from bauh.util import util
ignore_modules = {'{}_api'.format(__app_name__), '{}_commons'.format(__app_name__)}
ext_pattern = '{}_'.format(__app_name__)
def find_manager(member):
if not isinstance(member, str):
if inspect.isclass(member) and inspect.getmro(member)[1].__name__ == 'SoftwareManager':
return member
elif inspect.ismodule(member) and member.__name__ not in ignore_modules:
for name, mod in inspect.getmembers(member):
manager_found = find_manager(mod)
if manager_found:
return manager_found
def load_managers(locale: str, context: ApplicationContext) -> List[SoftwareManager]:
managers = []
for m in pkgutil.iter_modules():
if m.ispkg and m.name and m.name not in ignore_modules and m.name.startswith(ext_pattern):
module = pkgutil.find_loader(m.name).load_module()
if hasattr(module, 'controller'):
manager_class = find_manager(module.controller)
if manager_class:
locale_path = '{}/resources/locale'.format(module.__path__[0])
if os.path.exists(locale_path):
context.i18n.update(util.get_locale_keys(locale, locale_path))
managers.append(manager_class(context=context))
return managers

40
bauh/core/gems.py Normal file
View File

@@ -0,0 +1,40 @@
import inspect
import os
import pkgutil
from typing import List
from bauh import ROOT_DIR
from bauh.api.abstract.controller import SoftwareManager, ApplicationContext
from bauh.util import util
def find_manager(member):
if not isinstance(member, str):
if inspect.isclass(member) and inspect.getmro(member)[1].__name__ == 'SoftwareManager':
return member
elif inspect.ismodule(member):
for name, mod in inspect.getmembers(member):
manager_found = find_manager(mod)
if manager_found:
return manager_found
def load_managers(locale: str, context: ApplicationContext, names: List[str] = None) -> List[SoftwareManager]:
managers = []
for f in os.scandir(ROOT_DIR + '/gems'):
if f.is_dir() and f.name != '__pycache__' and (not names or f.name in names):
module = pkgutil.find_loader('bauh.gems.{}.controller'.format(f.name)).load_module()
manager_class = find_manager(module)
if manager_class:
locale_path = '{}/resources/locale'.format(f.path)
if os.path.exists(locale_path):
context.i18n.update(util.get_locale_keys(locale, locale_path))
managers.append(manager_class(context=context))
return managers

0
bauh/gems/__init__.py Normal file
View File

View File

@@ -0,0 +1,4 @@
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
BUILD_DIR = '/tmp/bauh/aur'

28
bauh/gems/arch/aur.py Normal file
View File

@@ -0,0 +1,28 @@
import re
from typing import Set, List
from bauh.api.http import HttpClient
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&'
URL_SEARCH = 'https://aur.archlinux.org/rpc/?v=5&type=search&arg='
def map_pkgbuild(pkgbuild: str) -> dict:
return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)}
class AURClient:
def __init__(self, http_client: HttpClient):
self.http_client = http_client
self.names_index = set()
def search(self, words: str) -> dict:
return self.http_client.get_json(URL_SEARCH + words)
def get_info(self, names: Set[str]) -> List[dict]:
res = self.http_client.get_json(URL_INFO + self._map_names_as_queries(names))
return res['results'] if res and res.get('results') else []
def _map_names_as_queries(self, names) -> str:
return '&'.join(['arg[{}]={}'.format(i, n) for i, n in enumerate(names)])

View File

@@ -0,0 +1,28 @@
from typing import Set
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.view import MultipleSelectComponent, InputOption
from bauh.commons.html import bold
def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> Set[str]:
view_opts = MultipleSelectComponent(label='',
options=[InputOption('{} ( {} )'.format(p, m.upper()), p) for p, m in
pkg_mirrors.items()])
install = watcher.request_confirmation(title=i18n['arch.install.optdeps.request.title'],
body='<p>{}</p>'.format(i18n['arch.install.optdeps.request.body'].format(bold(pkgname)) + ':'),
components=[view_opts],
confirmation_label=i18n['install'],
deny_label=i18n['cancel'])
if install:
return {o.value for o in view_opts.values}
def request_install_missing_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> bool:
deps_str = ''.join(['<br/><span style="font-weight:bold"> - {} ( {} )</span>'.format(d, m.upper()) for d, m in pkg_mirrors.items()])
msg = '<p>{}</p>'.format(i18n['arch.missing_deps.body'].format('<span style="font-weight: bold">{}</span>'.format(pkgname) + ':<br/>' + deps_str))
msg += i18n['ask.continue']
return watcher.request_confirmation(i18n['arch.missing_deps.title'], msg)

View File

@@ -0,0 +1,570 @@
import os
import re
import shutil
import time
from pathlib import Path
from threading import Thread
from typing import List, Set, Type
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion
from bauh.api.abstract.view import MessageType
from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess
from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions
from bauh.gems.arch.aur import AURClient
from bauh.gems.arch.mapper import ArchDataMapper
from bauh.gems.arch.model import ArchPackage
from bauh.commons.html import bold
from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater
URL_GIT = 'https://aur.archlinux.org/{}.git'
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz'
URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h='
RE_SPLIT_VERSION = re.compile(r'(=|>|<)')
class ArchManager(SoftwareManager):
def __init__(self, context: ApplicationContext):
super(ArchManager, self).__init__(context=context)
self.aur_cache = context.cache_factory.new()
# context.disk_loader_factory.map(ArchPackage, self.aur_cache) TODO
self.mapper = ArchDataMapper(http_client=context.http_client)
self.i18n = context.i18n
self.aur_client = AURClient(context.http_client)
self.names_index = {}
self.aur_index_updater = AURIndexUpdater(context, self)
self.dcache_updater = ArchDiskCacheUpdater(context.logger)
self.logger = context.logger
def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader):
app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'])
app.downgrade_enabled = downgrade_enabled
if app.installed:
res.installed.append(app)
if disk_loader:
disk_loader.fill(app)
else:
res.new.append(app)
Thread(target=self.mapper.fill_package_build, args=(app,)).start()
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
downgrade_enabled = git.is_enabled()
res = SearchResult([], [], 0)
installed = {}
read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()))
read_installed.start()
api_res = self.aur_client.search(words)
if api_res and api_res.get('results'):
read_installed.join()
for pkgdata in api_res['results']:
self._upgrade_search_result(pkgdata, installed, downgrade_enabled, res, disk_loader)
else: # if there are no results from the API (it could be because there were too many), tries the names index:
if self.names_index:
to_query = set()
for norm_name, real_name in self.names_index.items():
if words in norm_name:
to_query.add(real_name)
if len(to_query) == 25:
break
pkgsinfo = self.aur_client.get_info(to_query)
if pkgsinfo:
read_installed.join()
for pkgdata in pkgsinfo:
self._upgrade_search_result(pkgdata, installed, res)
res.total = len(res.installed) + len(res.new)
return res
def _fill_aur_pkgs(self, not_signed: dict, apps: list, disk_loader: DiskCacheLoader):
downgrade_enabled = git.is_enabled()
pkgsinfo = self.aur_client.get_info(not_signed.keys())
if pkgsinfo:
for pkgdata in pkgsinfo:
app = self.mapper.map_api_data(pkgdata, not_signed)
app.downgrade_enabled = downgrade_enabled
if disk_loader:
disk_loader.fill(app)
apps.append(app)
def _fill_mirror_pkgs(self, mirrors: dict, apps: list):
# TODO
for name, data in mirrors.items():
app = ArchPackage(name=name, version=data.get('version'), latest_version=data.get('version'), description=data.get('description'))
app.installed = True
app.mirror = '' # TODO
app.update = False # TODO
apps.append(app)
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
installed = pacman.list_and_map_installed()
apps = []
if installed and installed['not_signed']:
self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader)
return SearchResult(apps, None, len(apps))
def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
handler = ProcessHandler(watcher)
app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
watcher.change_progress(5)
try:
if not os.path.exists(app_build_dir):
build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir])))
if build_dir:
watcher.change_progress(10)
watcher.change_substatus(self.i18n['arch.clone'].format(bold(pkg.name)))
clone = handler.handle(SystemProcess(subproc=new_subprocess(['git', 'clone', URL_GIT.format(pkg.name)], cwd=app_build_dir), check_error_output=False))
watcher.change_progress(30)
if clone:
watcher.change_substatus(self.i18n['arch.downgrade.reading_commits'])
clone_path = '{}/{}'.format(app_build_dir, pkg.name)
pkgbuild_path = '{}/PKGBUILD'.format(clone_path)
commits = run_cmd("git log", cwd=clone_path)
watcher.change_progress(40)
if commits:
commit_list = re.findall(r'commit (.+)\n', commits)
if commit_list:
if len(commit_list) > 1:
for idx in range(1, len(commit_list)):
commit = commit_list[idx]
with open(pkgbuild_path) as f:
pkgdict = aur.map_pkgbuild(f.read())
if not handler.handle(SystemProcess(subproc=new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path), check_error_output=False)):
watcher.print('Could not downgrade anymore. Aborting...')
return False
if '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version:
# current version found
watcher.change_substatus(self.i18n['arch.downgrade.version_found'])
break
watcher.change_substatus(self.i18n['arch.downgrade.install_older'])
return self._make_pkg(pkg.name, root_password, handler, app_build_dir, clone_path, dependency=False, skip_optdeps=True)
else:
watcher.show_message(title=self.i18n['arch.downgrade.error'],
body=self.i18n['arch.downgrade.impossible'].format(pkg.name),
type_=MessageType.ERROR)
return False
watcher.show_message(title=self.i18n['error'], body=self.i18n['arch.downgrade.no_commits'], type_=MessageType.ERROR)
return False
finally:
if os.path.exists(app_build_dir):
handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', app_build_dir])))
return False
def clean_cache_for(self, pkg: ArchPackage):
if os.path.exists(pkg.get_disk_cache_path()):
shutil.rmtree(pkg.get_disk_cache_path())
def update(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
return self.install(pkg=pkg, root_password=root_password, watcher=watcher, skip_optdeps=True)
def _uninstall(self, pkg_name: str, root_password: str, handler: ProcessHandler) -> bool:
return handler.handle(SystemProcess(new_root_subprocess(['pacman', '-R', pkg_name, '--noconfirm'], root_password)))
def uninstall(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool:
handler = ProcessHandler(watcher)
watcher.change_progress(10)
info = pacman.get_info_dict(pkg.name)
watcher.change_progress(50)
if info.get('required by'):
pkname = '"{}"'.format(pkg.name)
msg = '{}:\n\n{}\n\n{}'.format(self.i18n['arch.uninstall.required_by'].format(pkname), info['required by'], self.i18n['arch.uninstall.required_by.advice'].format(pkname))
watcher.show_message(title=self.i18n['error'], body=msg, type_=MessageType.WARNING)
return False
uninstalled = self._uninstall(pkg.name, root_password, handler)
watcher.change_progress(100)
return uninstalled
def get_managed_types(self) -> Set["type"]:
return {ArchPackage}
def get_info(self, pkg: ArchPackage) -> dict:
if pkg.installed:
t = Thread(target=self.mapper.fill_package_build, args=(pkg,))
t.start()
info = pacman.get_info_dict(pkg.name)
t.join()
if pkg.pkgbuild:
info['13_pkg_build'] = pkg.pkgbuild
return info
else:
info = {
'01_id': pkg.id,
'02_name': pkg.name,
'03_version': pkg.version,
'04_popularity': pkg.popularity,
'05_votes': pkg.votes,
'06_package_base': pkg.package_base,
'07_maintainer': pkg.maintainer,
'08_first_submitted': pkg.first_submitted,
'09_last_modified': pkg.last_modified,
'10_url': pkg.url_download,
}
res_srcinfo = self.context.http_client.get(URL_SRC_INFO + pkg.name)
if res_srcinfo and res_srcinfo.text:
info['11_dependson'] = ' '.join(pkgbuild.read_depends_on(res_srcinfo.text))
info['12_optdepends'] = ' '.join(pkgbuild.read_optdeps(res_srcinfo.text))
if pkg.pkgbuild:
info['13_pkg_build'] = pkg.pkgbuild
else:
info['11_pkg_build_url'] = pkg.get_pkg_build_url()
return info
def get_history(self, pkg: ArchPackage) -> PackageHistory:
temp_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
try:
Path(temp_dir).mkdir(parents=True)
run_cmd('git clone ' + URL_GIT.format(pkg.name), print_error=False, cwd=temp_dir)
clone_path = '{}/{}'.format(temp_dir, pkg.name)
pkgbuild_path = '{}/PKGBUILD'.format(clone_path)
commits = git.list_commits(clone_path)
if commits:
history, status_idx = [], 0
for idx, commit in enumerate(commits):
with open(pkgbuild_path) as f:
pkgdict = aur.map_pkgbuild(f.read())
if '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version:
status_idx = idx
history.append({'1_version': pkgdict['pkgver'], '2_release': pkgdict['pkgrel'],
'3_date': commit['date']}) # the number prefix is to ensure the rendering order
if idx + 1 < len(commits):
if not run_cmd('git reset --hard ' + commits[idx + 1]['commit'], cwd=clone_path):
break
return PackageHistory(pkg=pkg, history=history, pkg_status_idx=status_idx)
finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
def _install_deps(self, deps: Set[str], pkg_mirrors: dict, root_password: str, handler: ProcessHandler, change_progress: bool = False) -> str:
"""
:param deps:
:param pkg_mirrors:
:param root_password:
:param handler:
:return: not installed dependency
"""
progress_increment = int(100 / len(deps))
progress = 0
self._update_progress(handler.watcher, 1, change_progress)
for pkgname in deps:
mirror = pkg_mirrors[pkgname]
handler.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ()'.format(pkgname, mirror))))
if mirror == 'aur':
installed = self._install_from_aur(pkgname=pkgname, root_password=root_password, handler=handler, dependency=True, change_progress=False)
else:
installed = self._install(pkgname=pkgname, root_password=root_password, handler=handler, install_file=None, mirror=mirror, change_progress=False)
if not installed:
return pkgname
progress += progress_increment
self._update_progress(handler.watcher, progress, change_progress)
self._update_progress(handler.watcher, 100, change_progress)
def _map_mirrors(self, pkgnames: Set[str]) -> dict:
pkg_mirrors = pacman.get_mirrors(pkgnames) # getting mirrors set
if len(pkgnames) != pkg_mirrors: # checking if any dep not found in the distro mirrors are from AUR
nomirrors = {p for p in pkgnames if p not in pkg_mirrors}
for pkginfo in self.aur_client.get_info(nomirrors):
if pkginfo.get('Name') in nomirrors:
pkg_mirrors[pkginfo['Name']] = 'aur'
return pkg_mirrors
def _make_pkg(self, pkgname: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool:
self._update_progress(handler.watcher, 50, change_progress)
if not self._install_missings_deps(pkgname, root_password, handler, project_dir):
return False
# building main package
handler.watcher.change_substatus(self.i18n['arch.building.package'].format(bold(pkgname)))
pkgbuilt = handler.handle(SystemProcess(new_subprocess(['makepkg', '-Acsmf'], cwd=project_dir)))
self._update_progress(handler.watcher, 65, change_progress)
if pkgbuilt:
gen_file = [fname for root, dirs, files in os.walk(build_dir) for fname in files if re.match(r'^{}-.+\.tar\.xz'.format(pkgname), fname)]
if not gen_file:
handler.watcher.print('Could not find generated .tar.xz file. Aborting...')
return False
install_file = '{}/{}'.format(project_dir, gen_file[0])
if self._install(pkgname=pkgname, root_password=root_password, mirror='aur', handler=handler, install_file=install_file, pkgdir=project_dir, change_progress=change_progress):
if dependency or skip_optdeps:
return True
handler.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(pkgname)))
if self._install_optdeps(pkgname, root_password, handler, project_dir, change_progress=change_progress):
return True
return False
def _install_missings_deps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool:
handler.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(pkgname)))
missing_deps = makepkg.check_missing_deps(pkgdir, handler.watcher)
if missing_deps:
depnames = {RE_SPLIT_VERSION.split(dep)[0] for dep in missing_deps}
dep_mirrors = self._map_mirrors(depnames)
for dep in depnames: # cheking if a dependency could not be found in any mirror
if dep not in dep_mirrors:
message.show_dep_not_found(dep, self.i18n, handler.watcher)
return False
handler.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(pkgname)))
if not confirmation.request_install_missing_deps(pkgname, dep_mirrors, handler.watcher, self.i18n):
handler.watcher.print(self.i18n['action.cancelled'])
return False
dep_not_installed = self._install_deps(depnames, dep_mirrors, root_password, handler, change_progress=False)
if dep_not_installed:
message.show_dep_not_installed(handler.watcher, pkgname, dep_not_installed, self.i18n)
return False
return True
def _install_optdeps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str, change_progress: bool = True) -> bool:
with open('{}/.SRCINFO'.format(pkgdir)) as f:
odeps = pkgbuild.read_optdeps(f.read())
if not odeps:
return True
installed = pacman.list_installed()
to_install = {d for d in odeps if d not in installed}
if not to_install:
return True
pkg_mirrors = self._map_mirrors(to_install)
if pkg_mirrors:
deps_to_install = confirmation.request_optional_deps(pkgname, pkg_mirrors, handler.watcher, self.i18n)
if not deps_to_install:
return True
else:
dep_not_installed = self._install_deps(deps_to_install, pkg_mirrors, root_password, handler, change_progress=True)
if dep_not_installed:
message.show_optdep_not_installed(dep_not_installed, handler.watcher, self.i18n)
return False
return True
def _install(self, pkgname: str, root_password: str, mirror: str, handler: ProcessHandler, install_file: str = None, pkgdir: str = '.', change_progress: bool = True):
check_install_output = []
pkgpath = install_file if install_file else pkgname
handler.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(pkgname)))
for check_out in new_root_subprocess(['pacman', '-U', pkgpath], root_password=root_password, cwd=pkgdir).stdout:
if check_out:
check_install_output.append(check_out.decode())
self._update_progress(handler.watcher, 70, change_progress)
if check_install_output and 'conflict' in check_install_output[-1]:
conflicting_apps = [w[0] for w in re.findall(r'((\w|\-|\.)+)\s(and|are)', check_install_output[-1])]
conflict_msg = ' {} '.format(self.i18n['and']).join(conflicting_apps)
if not handler.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'],
body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)):
handler.watcher.print(self.i18n['action.cancelled'])
return False
else: # uninstall conflicts
self._update_progress(handler.watcher, 75, change_progress)
to_uninstall = [conflict for conflict in conflicting_apps if conflict != pkgname]
for conflict in to_uninstall:
handler.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflict)))
if not self._uninstall(conflict, root_password, handler):
handler.watcher.show_message(title=self.i18n['error'],
body=self.i18n['arch.uninstalling.conflict.fail'].format('"{}"'.format(conflict)),
type_=MessageType.ERROR)
return False
handler.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(pkgname)))
self._update_progress(handler.watcher, 80, change_progress)
installed = handler.handle(pacman.install_as_process(pkgpath=pkgpath, root_password=root_password, aur=install_file is not None, pkgdir=pkgdir))
self._update_progress(handler.watcher, 95, change_progress)
if installed and self.context.disk_cache:
handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname)))
disk.save(pkgname, mirror)
self._update_progress(handler.watcher, 100, change_progress)
return installed
def _update_progress(self, watcher: ProcessWatcher, val: int, change_progress: bool):
if change_progress:
watcher.change_progress(val)
def _install_from_aur(self, pkgname: str, root_password: str, handler: ProcessHandler, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool:
app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
try:
if not os.path.exists(app_build_dir):
build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir])))
self._update_progress(handler.watcher, 10, change_progress)
if build_dir:
file_url = URL_PKG_DOWNLOAD.format(pkgname)
file_name = file_url.split('/')[-1]
handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.downloading.package'], bold(file_name)))
download = handler.handle(SystemProcess(new_subprocess(['wget', file_url], cwd=app_build_dir)))
if download:
self._update_progress(handler.watcher, 30, change_progress)
handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.uncompressing.package'], bold(file_name)))
uncompress = handler.handle(SystemProcess(new_subprocess(['tar', 'xvzf', '{}.tar.gz'.format(pkgname)], cwd=app_build_dir)))
self._update_progress(handler.watcher, 40, change_progress)
if uncompress:
uncompress_dir = '{}/{}'.format(app_build_dir, pkgname)
return self._make_pkg(pkgname=pkgname,
root_password=root_password,
handler=handler,
build_dir=app_build_dir,
project_dir=uncompress_dir,
dependency=dependency,
skip_optdeps=skip_optdeps,
change_progress=change_progress)
finally:
if os.path.exists(app_build_dir):
handler.handle(SystemProcess(new_subprocess(['rm', '-rf', app_build_dir])))
return False
def install(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher, skip_optdeps: bool = False) -> bool:
return self._install_from_aur(pkg.name, root_password, ProcessHandler(watcher), dependency=False, skip_optdeps=skip_optdeps)
def _is_wget_available(self):
try:
new_subprocess(['wget', '--version'])
return True
except FileNotFoundError:
return False
def is_enabled(self) -> bool:
try:
return pacman.is_enabled() and self._is_wget_available()
except FileNotFoundError:
return False
def is_downgrade_enabled(self) -> bool:
try:
new_subprocess(['git', '--version'])
return True
except FileNotFoundError:
return False
def cache_to_disk(self, pkg: ArchPackage, icon_bytes: bytes, only_icon: bool):
if self.context.disk_cache and pkg.supports_disk_cache():
pass
def requires_root(self, action: str, pkg: ArchPackage):
return action != 'search'
def prepare(self):
self.dcache_updater.start()
self.aur_index_updater.start()
def list_updates(self) -> List[PackageUpdate]:
return [PackageUpdate(app.id, app.latest_version, 'aur') for app in self.read_installed(disk_loader=None).installed if app.update]
def list_warnings(self) -> List[str]:
warnings = []
if not pacman.is_enabled():
warnings.append(self.i18n['arch.warning.disabled'].format(bold('pacman')))
if not self._is_wget_available():
warnings.append(self.i18n['arch.warning.disabled'].format(bold('wget')))
if not git.is_enabled():
warnings.append(self.i18n['arch.warning.git'].format(bold('git')))
return warnings
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
res = []
sugs = [(i, p) for i, p in suggestions.ALL.items()]
sugs.sort(key=lambda t: t[1].value, reverse=True)
if limit > 0:
sugs = sugs[0:limit]
sug_names = {s[0] for s in sugs}
api_res = self.aur_client.get_info(sug_names)
if api_res:
for pkg in api_res:
if pkg.get('Name') in sug_names:
res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}), suggestions.ALL.get(pkg['Name'])))
return res

179
bauh/gems/arch/disk.py Normal file
View File

@@ -0,0 +1,179 @@
import json
import os
import re
from pathlib import Path
from typing import Set, List
from bauh.gems.arch import pacman
from bauh.gems.arch.model import ArchPackage
RE_DESKTOP_ENTRY = re.compile(r'(Exec|Icon)\s*=\s*(.+)')
RE_CLEAN_NAME = re.compile(r'^(\w+)-?|_?.+')
def write(app: ArchPackage):
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(app.get_data_to_cache()))
def fill_icon_path(app: ArchPackage, icon_paths: List[str], only_exact_match: bool):
ends_with = re.compile(r'.+/{}\.(png|svg)$'.format(app.name), re.IGNORECASE)
for path in icon_paths:
if ends_with.match(path):
app.icon_path = path
return
if not only_exact_match:
pkg_icons_path = pacman.list_icon_paths({app.name})
if pkg_icons_path:
app.icon_path = pkg_icons_path[0]
def set_icon_path(app: ArchPackage, icon_name: str = None):
installed_icons = pacman.list_icon_paths(app.name)
if installed_icons:
exact_match = re.compile(r'.+/{}\..+$'.format(icon_name.split('.')[0] if icon_name else app.name))
for icon_path in installed_icons:
if exact_match.match(icon_path):
app.icon_path = icon_path
break
def save(pkgname: str, mirror: str):
app = ArchPackage(name=pkgname, mirror=mirror)
desktop_files = pacman.list_desktop_entries({pkgname})
if desktop_files:
if len(desktop_files) > 1:
exact_match = [f for f in desktop_files if f.endswith('{}.desktop'.format(pkgname))]
to_parse = exact_match[0] if exact_match else desktop_files[0]
else:
to_parse = desktop_files[0]
app.desktop_entry = to_parse
with open(to_parse) as f:
desktop_entry = f.read()
for field in RE_DESKTOP_ENTRY.findall(desktop_entry):
if field[0] == 'Exec':
app.command = field[1].strip()
elif field[0] == 'Icon':
app.icon_path = field[1].strip()
if app.icon_path and '/' not in app.icon_path: # if the icon full path is not defined
set_icon_path(app, app.icon_path)
else:
bin_paths = pacman.list_bin_paths({pkgname})
if bin_paths:
if len(bin_paths) > 1:
exact_match = [p for p in bin_paths if p.endswith('/' + pkgname)]
app.command = exact_match[0] if exact_match else bin_paths[0]
else:
app.command = bin_paths[0]
set_icon_path(app)
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(app.get_data_to_cache()))
def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True) -> int:
to_cache = {n for n in pkgnames if overwrite or not os.path.exists(ArchPackage.disk_cache_path(n, mirror))}
desktop_files = pacman.list_desktop_entries(to_cache)
no_desktop_files = {}
to_write = []
if desktop_files:
desktop_matches, no_exact_match = {}, set()
for pkg in to_cache: # first try to find exact matches
ends_with = re.compile('.+/{}.desktop$'.format(pkg), re.IGNORECASE)
for f in desktop_files:
if ends_with.match(f):
desktop_matches[pkg] = f
break
if pkg not in desktop_matches:
no_exact_match.add(pkg)
if no_exact_match: # check every not matched app individually
for pkg in no_exact_match:
entries = pacman.list_desktop_entries({pkg})
if entries:
desktop_matches[pkg] = entries[0]
if not desktop_matches:
no_desktop_files = to_cache
else:
if len(desktop_matches) != len(to_cache):
no_desktop_files = {p for p in to_cache if p not in desktop_matches}
pkgs, apps_icons_noabspath = [], []
for pkgname, file in desktop_matches.items():
p = ArchPackage(name=pkgname, mirror=mirror)
p.desktop_entry = file
with open(file) as f:
desktop_entry = f.read()
for field in RE_DESKTOP_ENTRY.findall(desktop_entry):
if field[0] == 'Exec':
p.command = field[1].strip()
elif field[0] == 'Icon':
p.icon_path = field[1].strip()
if p.icon_path and '/' not in p.icon_path: # if the icon full path is not defined
apps_icons_noabspath.append(p)
pkgs.append(p)
if apps_icons_noabspath:
icon_paths = pacman.list_icon_paths({app.name for app in apps_icons_noabspath})
if icon_paths:
for p in apps_icons_noabspath:
fill_icon_path(p, icon_paths, False)
for p in pkgs:
to_write.append(p)
if no_desktop_files:
pkgs = {ArchPackage(name=n, mirror=mirror) for n in no_desktop_files}
bin_paths = pacman.list_bin_paths(no_desktop_files)
if bin_paths:
for p in pkgs:
ends_with = re.compile(r'.+/{}$'.format(p.name), re.IGNORECASE)
for path in bin_paths:
if ends_with.match(path):
p.command = path
break
icon_paths = pacman.list_icon_paths(no_desktop_files)
if icon_paths:
for p in pkgs:
fill_icon_path(p, icon_paths, only_exact_match=True)
for p in pkgs:
to_write.append(p)
if to_write:
for p in to_write:
write(p)
return len(to_write)
return 0

29
bauh/gems/arch/git.py Normal file
View File

@@ -0,0 +1,29 @@
from datetime import datetime
from typing import List
from bauh.commons.system import new_subprocess
def is_enabled() -> bool:
try:
new_subprocess(['git', '--version'])
return True
except FileNotFoundError:
return False
def list_commits(proj_dir:str) -> List[dict]:
logs = new_subprocess(['git', 'log', '--date=iso'], cwd=proj_dir).stdout
commits, commit = [], {}
for out in new_subprocess(['grep', '-E', 'commit|Date:'], stdin=logs).stdout:
if out:
line = out.decode()
if line.startswith('commit'):
commit['commit'] = line.split(' ')[1].strip()
elif line.startswith('Date'):
commit['date'] = datetime.fromisoformat(line.split(':')[1].strip())
commits.append(commit)
commit = {}
return commits

28
bauh/gems/arch/makepkg.py Normal file
View File

@@ -0,0 +1,28 @@
import re
from typing import List
from bauh.api.abstract.handler import ProcessWatcher
from bauh.commons.system import new_subprocess
RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n')
def check_missing_deps(pkgdir: str, watcher: ProcessWatcher) -> List[str]:
depcheck = new_subprocess(['makepkg', '--check'], cwd=pkgdir)
for o in depcheck.stdout:
if o:
watcher.print(o.decode())
error_lines = []
for s in depcheck.stderr:
if s:
line = s.decode()
print(line)
error_lines.append(line)
if error_lines:
error_str = ''.join(error_lines)
if 'Missing dependencies' in error_str:
return RE_DEPS_PATTERN.findall(error_str)

55
bauh/gems/arch/mapper.py Normal file
View File

@@ -0,0 +1,55 @@
from datetime import datetime
from bauh.api.http import HttpClient
from bauh.gems.arch.model import ArchPackage
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}'
class ArchDataMapper:
def __init__(self, http_client: HttpClient):
self.http_client = http_client
def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = True):
version = package.get('Version')
if version:
version = version.split(':')
version = version[0] if len(version) == 1 else version[1]
pkg.id = package.get('ID')
pkg.name = package.get('Name')
if fill_version:
pkg.version = version
pkg.latest_version = version
pkg.description = package.get('Description')
pkg.package_base = package.get('PackageBase')
pkg.popularity = package.get('Popularity')
pkg.votes = package.get('NumVotes')
pkg.maintainer = package.get('Maintainer')
pkg.url_download = URL_PKG_DOWNLOAD.format(package['URLPath']) if package.get('URLPath') else None
pkg.first_submitted = datetime.fromtimestamp(package['FirstSubmitted']) if package.get('FirstSubmitted') else None
pkg.last_modified = datetime.fromtimestamp(package['LastModified']) if package.get('LastModified') else None
pkg.update = pkg.version and pkg.latest_version and pkg.latest_version > pkg.version
def fill_package_build(self, pkg: ArchPackage):
res = self.http_client.get(pkg.get_pkg_build_url())
if res and res.status_code == 200 and res.text:
pkg.pkgbuild = res.text
def map_api_data(self, apidata: dict, installed: dict) -> ArchPackage:
data = installed.get(apidata.get('Name'))
app = ArchPackage(name=apidata.get('Name'), installed=bool(data), mirror='aur')
if data:
app.version = data.get('version')
app.description = data.get('description')
self.fill_api_data(app, apidata, fill_version=not data)
return app

20
bauh/gems/arch/message.py Normal file
View File

@@ -0,0 +1,20 @@
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.view import MessageType
def show_dep_not_installed(watcher: ProcessWatcher, pkgname: str, depname: str, i18n: dict):
watcher.show_message(title=i18n['error'],
body=i18n['arch.install.dependency.install.error'].format('"{}"'.format(depname), '"{}"'.format(pkgname)),
type_=MessageType.ERROR)
def show_dep_not_found(depname: str, i18n: dict, watcher: ProcessWatcher):
watcher.show_message(title=i18n['arch.install.dep_not_found.title'],
body=i18n['arch.install.dep_not_found.body'].format('"{}"'.format(depname)),
type_=MessageType.ERROR)
def show_optdep_not_installed(depname: str, watcher: ProcessWatcher, i18n: dict):
watcher.show_message(title=i18n['error'],
body=i18n['arch.install.optdep.error'].format('"{}"'.format(depname)),
type_=MessageType.ERROR)

109
bauh/gems/arch/model.py Normal file
View File

@@ -0,0 +1,109 @@
import datetime
from bauh.api.abstract.model import SoftwarePackage
from bauh.api.constants import CACHE_PATH
from bauh.commons import resource
from bauh.gems.arch import ROOT_DIR
class ArchPackage(SoftwarePackage):
def __init__(self, name: str = None, version: str = None, latest_version: str = None, description: str = None,
package_base: str = None, votes: int = None, popularity: float = None,
first_submitted: datetime.datetime = None, last_modified: datetime.datetime = None,
maintainer: str = None, url_download: str = None, pkgbuild: str = None, mirror: str = None,
desktop_entry: str = None, installed: bool = False):
super(ArchPackage, self).__init__(name=name, version=version, latest_version=latest_version, description=description, installed=installed)
self.package_base = package_base
self.votes = votes
self.popularity = popularity
self.maintainer = maintainer
self.url_download = url_download
self.first_submitted = first_submitted
self.last_modified = last_modified
self.pkgbuild = pkgbuild
self.mirror = mirror
self.command = None
self.icon_path = None
self.downgrade_enabled = False
self.desktop_entry = desktop_entry
@staticmethod
def disk_cache_path(pkgname: str, mirror: str):
return CACHE_PATH + '/arch/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname
def get_pkg_build_url(self):
if self.package_base:
return 'https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=' + self.package_base
def has_history(self):
return self.installed
def has_info(self):
return True
def can_be_installed(self):
return super(ArchPackage, self).can_be_installed() and self.url_download
def can_be_downgraded(self):
return self.installed and self.downgrade_enabled
def get_type(self):
return 'aur' if self.mirror == 'aur' else 'arch'
def get_default_icon_path(self) -> str:
return self.get_type_icon_path()
def get_disk_icon_path(self) -> str:
return self.icon_path
def get_type_icon_path(self):
return resource.get_path('img/arch.png', ROOT_DIR) # TODO change icon when from mirrors
def is_application(self):
return True
def supports_disk_cache(self):
return True
def get_disk_cache_path(self) -> str:
if self.name:
return self.disk_cache_path(self.name, self.mirror)
def get_data_to_cache(self) -> dict:
cache = {}
# required attrs to cache
for a in {'command', 'icon_path', 'mirror'}:
val = getattr(self, a)
if val:
cache[a] = val
else:
return None
if self.desktop_entry:
cache['desktop_entry'] = self.desktop_entry
return cache
def fill_cached_data(self, data: dict):
for a in {'command', 'icon_path', 'mirror', 'desktop_entry'}:
val = data.get(a)
if val:
setattr(self, a, val)
if a == 'icon_path':
self.icon_url = val
def can_be_run(self) -> bool:
# only returns if there is a desktop entry set for the application to avoid running command-line applications
return bool(self.desktop_entry)
def get_command(self) -> str:
return self.command
def get_publisher(self):
return self.maintainer

131
bauh/gems/arch/pacman.py Normal file
View File

@@ -0,0 +1,131 @@
import re
from typing import List, Set
from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess
def is_enabled() -> bool:
try:
new_subprocess(['pacman', '--version'])
return True
except FileNotFoundError:
return False
def get_mirrors(pkgs: Set[str]) -> dict:
pkgre = '|'.join(pkgs)
searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout
mirrors = {}
for line in new_subprocess(['grep', '-E', '.+/({}) '.format(pkgre)], stdin=searchres).stdout:
if line:
match = line.decode()
for p in pkgs:
if p in match:
mirrors[p] = match.split('/')[0]
return mirrors
def is_available_from_mirrors(pkg_name: str) -> bool:
return bool(run_cmd('pacman -Ss ' + pkg_name))
def get_info(pkg_name) -> str:
return run_cmd('pacman -Qi ' + pkg_name)
def get_info_list(pkg_name: str) -> List[tuple]:
info = get_info(pkg_name)
if info:
return re.findall(r'(\w+\s?\w+)\s+:\s+(.+(\n\s+.+)*)', info)
def get_info_dict(pkg_name: str) -> dict:
info_list = get_info_list(pkg_name)
if info_list:
info_dict = {}
for info_data in info_list:
attr = info_data[0].lower()
info_dict[attr] = info_data[1] if '\n' not in info_data[1] else ' '.join([l.strip() for l in info_data[1].split('\n')])
if info_dict[attr] == 'None':
info_dict[attr] = None
return info_dict
def list_installed() -> Set[str]:
return {out.decode().strip() for out in new_subprocess(['pacman', '-Qq']).stdout if out}
def list_and_map_installed() -> dict: # returns a dict with with package names as keys and versions as values
installed = new_subprocess(['pacman', '-Qq']).stdout # retrieving all installed package names
allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info
pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {}
for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], stdin=allinfo).stdout: # filtering only the Name and Validated By fields:
if out:
line = out.decode()
if line.startswith('Name'):
current_pkg['name'] = line.split(':')[1].strip()
elif line.startswith('Version'):
version = line.split(':')
current_pkg['version'] = version[len(version) - 1].strip()
elif line.startswith('Description'):
current_pkg['description'] = line.split(':')[1].strip()
elif line.startswith('Validated'):
if line.split(':')[1].strip().lower() == 'none':
pkgs['not_signed'][current_pkg['name']] = {'version': current_pkg['version'], 'description': current_pkg['description']}
current_pkg = {}
return pkgs
def install_as_process(pkgpath: str, root_password: str, aur: bool, pkgdir: str = '.') -> SystemProcess:
if aur:
cmd = ['pacman', '-U', pkgpath, '--noconfirm'] # pkgpath = install file path
else:
cmd = ['pacman', '-S', pkgpath, '--noconfirm'] # pkgpath = pkgname
return SystemProcess(new_root_subprocess(cmd, root_password, cwd=pkgdir))
def list_desktop_entries(pkgnames: Set[str]) -> List[str]:
if pkgnames:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
desktop_files = []
for out in new_subprocess(['grep', '-E', '.desktop'], stdin=installed_files).stdout:
if out:
desktop_files.append(out.decode().strip())
return desktop_files
def list_icon_paths(pkgnames: Set[str]) -> List[str]:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
icon_files = []
for out in new_subprocess(['grep', '-E', '.(png|svg|jpeg)'], stdin=installed_files).stdout:
if out:
icon_files.append(out.decode().strip())
return icon_files
def list_bin_paths(pkgnames: Set[str]) -> List[str]:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]).stdout
bin_paths = []
for out in new_subprocess(['grep', '-E', '^/usr/bin/.+'], stdin=installed_files).stdout:
if out:
bin_paths.append(out.decode().strip())
return bin_paths

View File

@@ -0,0 +1,13 @@
import re
from typing import Set
RE_PKGBUILD_OPTDEPS = re.compile(r"optdepends = ([^:\s]+)")
RE_PKGBUILD_DEPSON = re.compile(r"depends = ([^:\s]+)")
def read_optdeps(srcinfo: str) -> Set[str]:
return set(RE_PKGBUILD_OPTDEPS.findall(srcinfo))
def read_depends_on(srcinfo: str) -> Set[str]:
return set(RE_PKGBUILD_DEPSON.findall(srcinfo))

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

View File

@@ -0,0 +1,49 @@
gem.arch.label=Arch ( AUR )
arch.install.conflict.popup.title=Conflict detected
arch.install.conflict.popup.body=The applications {} are in conflict. You must uninstall one to install the other. Continue ?
arch.missing_deps.title=Missing dependencies
arch.missing_deps.body=The following dependencies must be installed before the {} installation continues
arch.downgrade.error=Error
arch.downgrade.impossible=It is not possible to downgrade {}
aur.history.1_version=version
aur.history.2_release=release
aur.history.3_date=date
arch.downloading.package=Downloading the package
arch.uncompressing.package=Uncompressing the package
arch.checking.deps=Checking {} dependencies
arch.missing_deps_found=Missing dependencies for {}
arch.building.package=Building package {}
arch.checking.conflicts=Checking any conflicts with {}
arch.installing.package=Installing {} package
arch.uninstalling.conflict=Uninstalling conflicting package {}
arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting package {}
arch.clone=Cloning the AUR repository {}
arch.downgrade.reading_commits=Reading the repository commits
arch.downgrade.version_found=Current package version found
arch.downgrade.install_older=Installing older version
aur.info.01_id=id
aur.info.02_name=name
aur.info.03_version=version
aur.info.04_popularity=popularity
aur.info.05_votes=votes
aur.info.06_package_base=package base
aur.info.07_maintainer=maintainer
aur.info.08_first_submitted=first submitted
aur.info.09_last_modified=app.last_modified
aur.info.10_url=url download
aur.info.11_pkg_build_url=url pkgbuild
aur.info.13_pkg_build=pkgbuild
aur.info.11_dependson=dependencies
aur.info.12_optdepends=optional dependencies
arch.install.dep_not_found.title=Dependency not found
arch.install.dep_not_found.body=Required dependency {} was not found in AUR nor in default mirrors. Installation cancelled.
arch.install.dependency.install=Installing package dependency {}
arch.install.dependency.install.error=Could not install dependent package {}. Installation of {} aborted.
arch.uninstall.required_by={} cannot be uninstalled because it is necessary for these following packages to work
arch.uninstall.required_by.advice=Uninstall them first before uninstalling {}.
arch.install.optdeps.request.title=Optional dependencies
arch.install.optdeps.request.body={} was succesfully installed ! There are some optional associated packages that you might want to install as well (check those you want)
arch.install.optdep.error=Could not install the optional package {}
arch.optdeps.checking=Checking {} optional dependencies
arch.warning.disabled={} seems not to be installed. It will not be possible to manage Arch / AUR packages.
arch.warning.git={} seems not to be installed. It will not be possible to downgrade AUR packages.

View File

@@ -0,0 +1,81 @@
gem.arch.label=Arch ( AUR )
aur.info.architecture=arquitectura
aur.info.architecture.any=cualquier
aur.info.build date=fecha de construcción
aur.info.depends on=depende
aur.info.description=descripción
aur.info.install date=fecha de instalación
aur.info.install reason=razón da instalación
aur.info.install reason.explicitly installed=instalado explícitamente
aur.info.install script=script de instalación
aur.info.install script.no=ninguno
aur.info.installed size=tamaño da instalación
aur.info.licenses=licencias
aur.info.name=nombre
aur.info.optional deps=dependencias opcionales
aur.info.packager=empaquetador
aur.info.packager.unknown packager=desconocido
aur.info.url=url
aur.info.version=versión
aur.info.arch=arquitectura
aur.info.arch.any=cualquier
aur.info.depends=depende
aur.info.pkgdesc=descripción
aur.info.pkgname=nombre
aur.info.pkgrel=lanzamiento
aur.info.pkgver=versión
aur.info.source=origen
aur.info.optdepends=dependencias opcionales
aur.info.license=licencia
aur.info.validpgpkeys=llaves PGP válidas
aur.info.options=opciones
aur.info.provides=provee
aur.info.conflicts with=conflicta
arch.install.conflict.popup.title=Conflicto detectado
arch.install.conflict.popup.body=Los aplicativos {} estan en conflicto. Debe desinstalar uno para instalar el otro. ¿Continuar?
arch.missing_deps.title=Dependencias faltantes
arch.missing_deps.body=Deben instalarse las siguientes dependencias antes de continuar la instalación de {}
arch.downgrade.error=Error
arch.downgrade.impossible=No es posible revertir la versión de {}
aur.history.1_version=versión
aur.history.2_release=lanzamiento
aur.history.3_date=fecha
arch.downloading.package=Descargando el paquete
arch.uncompressing.package=Descomprimindo el paquete
arch.checking.deps=Verificando las dependencias de {}
arch.missing_deps_found=Dependencias faltantes para {}
arch.building.package=Construyendo el paquete {}
arch.checking.conflicts=Verificando se hay conflictos con {}
arch.installing.package=Instalando el paquete {}
arch.uninstalling.conflict=Eliminando el paquete conflictivo {}
arch.uninstalling.conflict.fail=No fue posible desinstalar el paquete conflictivo {}
arch.clone=Clonando el repositorio {} de AUR
arch.downgrade.reading_commits=Leyendo los commits del repositorio
arch.downgrade.version_found=Version actual del paquete encontrada
arch.downgrade.install_older=Instalando versión anterior
aur.info.01_id=id
aur.info.02_name=nombre
aur.info.03_version=versión
aur.info.04_popularity=popularidad
aur.info.05_votes=votos
aur.info.06_package_base=paquete base
aur.info.07_maintainer=mantenedor
aur.info.08_first_submitted=primero envio
aur.info.09_last_modified=última modificación
aur.info.10_url=url download
aur.info.11_pkg_build_url=url pkgbuild
aur.info.13_pkg_build=pkgbuild
aur.info.11_dependson=dependencias
aur.info.12_optdepends=dependencias opcionales
arch.install.dep_not_found.title=Dependencia no encontrada
arch.install.dep_not_found.body=No se encontró la dependencia requerida {} en AUR ni en los espejos predeterminados. Instalación cancelada.
arch.install.dependency.install=Instalando el paquete dependiente {}
arch.install.dependency.install.error=No se pudo instalar el paquete dependiente {}. Instalación de {} abortada.
arch.uninstall.required_by=No se puede desinstalar {} porque es necesario para que los siguientes paquetes funcionen
arch.uninstall.required_by.advice=Debe desinstalarlos primero antes de desinstalar {}
arch.install.optdeps.request.title=Dependencias opcionales
arch.install.optdeps.request.body=¡{} se instaló correctamente! También hay algunos paquetes opcionales asociados que es posible que desee instalar (marque los que desee)
arch.install.optdep.error=No se pudo instalar el paquete opcional {}
arch.optdeps.checking=Verificando las dependencias opcionales de {}
arch.warning.disabled={} parece no estar instalado. No será posible administrar paquetes Arch / AUR.
arch.warning.git={} parece no estar instalado. No será posible revertir las versiones de paquetes Arch / AUR.

View File

@@ -0,0 +1,81 @@
gem.arch.label=Arch ( AUR )
aur.info.architecture=arquitetura
aur.info.architecture.any=qualquer
aur.info.build date=data de construção
aur.info.depends on=depende
aur.info.description=descrição
aur.info.install date=data de instalação
aur.info.install reason=razão da instalação
aur.info.install reason.explicitly installed=instalado explicitamente
aur.info.install script=script de instalação
aur.info.install script.no=nenhum
aur.info.installed size=tamanho da instalação
aur.info.licenses=licenças
aur.info.name=nome
aur.info.optional deps=dependências opcionais
aur.info.packager=empacotador
aur.info.packager.unknown packager=desconhecido
aur.info.url=url
aur.info.version=versão
aur.info.arch=arquitetura
aur.info.arch.any=qualquer
aur.info.depends=depende
aur.info.pkgdesc=descrição
aur.info.pkgname=nome
aur.info.pkgrel=lançamento
aur.info.pkgver=versão
aur.info.source=origem
aur.info.optdepends=dependências opcionais
aur.info.license=licença
aur.info.validpgpkeys=chaves PGP válidas
aur.info.options=opções
aur.info.provides=provê
aur.info.conflicts with=conflita
arch.install.conflict.popup.title=Conflito detectado
arch.install.conflict.popup.body=Os aplicativos {} estão em conflito. Você precisa desinstalar um para instalar o outro. Continuar ?
arch.missing_deps.title=Dependências ausentes
arch.missing_deps.body=As seguintes dependências devem ser instaladas antes de continuar com a instalação de {}
arch.downgrade.error=Erro
arch.downgrade.impossible=Não é possível reverter a versão de {}
aur.history.1_version=versão
aur.history.2_release=lançamento
aur.history.3_date=data
arch.downloading.package=Baixando o pacote
arch.uncompressing.package=Descompactando o pacote
arch.checking.deps=Checando as dependências de {}
arch.missing_deps_found=Dependencias ausentes para {}
arch.building.package=Construindo o pacote {}
arch.checking.conflicts=Verificando se há conflitos com {}
arch.installing.package=Instalando o pacote {}
arch.uninstalling.conflict=Desinstalando o pacote conflitante {}
arch.uninstalling.conflict.fail=Não foi possível desinstalar o pacote conflitante {}
arch.clone=Clonando o repositório {} do AUR
arch.downgrade.reading_commits=Lendo os commits do repositório
arch.downgrade.version_found=Versão atual do pacote encontrada
arch.downgrade.install_older=Instalando versão anterior
aur.info.01_id=id
aur.info.02_name=nome
aur.info.03_version=versão
aur.info.04_popularity=popularidade
aur.info.05_votes=votos
aur.info.06_package_base=pacote base
aur.info.07_maintainer=mantenedor
aur.info.08_first_submitted=primeira submissão
aur.info.09_last_modified=última modificação
aur.info.10_url=url download
aur.info.11_pkg_build_url=url pkgbuild
aur.info.13_pkg_build=pkgbuild
aur.info.11_dependson=dependências
aur.info.12_optdepends=dependências opcionais
arch.install.dep_not_found.title=Dependência não encontrada
arch.install.dep_not_found.body=A dependência {} não foi encontrado no AUR nem nos espelhos padrões. Instalação cancelada.
arch.install.dependency.install=Instalando o pacote dependente {}
arch.install.dependency.install.error=Não foi possível instalar o pacote dependente {}. Instalação de {} abortada.
arch.uninstall.required_by={} não pode ser desinstalado porque ele é necessário para o funcionamento dos seguintes pacotes
arch.uninstall.required_by.advice=Desinstale eles primeiro antes de desinstalar {}
arch.install.optdeps.request.title=Dependências opcionais
arch.install.optdeps.request.body={} foi instalado com sucesso ! Existem alguns pacotes opcionais associados que talvez você também queira instalar (marque os desejados)
arch.install.optdep.error=Não foi possível instalar o pacote opcional {}
arch.optdeps.checking=Verificando as dependências opcionais de {}
arch.warning.disabled={} parece não estar instalado. Não será possível gerenciar pacotes Arch / AUR.
arch.warning.git={} parece não estar instalado. Não será possível reverter versões de pacotes Arch / AUR.

View File

@@ -0,0 +1,3 @@
from bauh.api.abstract.model import SuggestionPriority
ALL = {'google-chrome': SuggestionPriority.HIGH}

53
bauh/gems/arch/worker.py Normal file
View File

@@ -0,0 +1,53 @@
import logging
import os
import time
from multiprocessing import Process
from threading import Thread
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.gems.arch import pacman, disk
URL_INDEX = 'https://aur.archlinux.org/packages.gz'
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&arg={}'
class AURIndexUpdater(Thread):
def __init__(self, context: ApplicationContext, man: SoftwareManager):
super(AURIndexUpdater, self).__init__(daemon=True)
self.http_client = context.http_client
self.logger = context.logger
self.man = man
def run(self):
while True:
self.logger.info('Pre-indexing AUR packages in memory')
res = self.http_client.get(URL_INDEX)
if res and res.text:
self.man.names_index = {n.replace('-', '').replace('_', '').replace('.', ''): n for n in res.text.split('\n') if n and not n.startswith('#')}
self.logger.info('Pre-indexed {} AUR package names in memory'.format(len(self.man.names_index)))
else:
self.logger.warning('No data returned from: {}'.format(URL_INDEX))
time.sleep(5 * 60) # updates every 5 minutes
class ArchDiskCacheUpdater(Thread if bool(os.getenv('BAUH_DEBUG', 0)) else Process):
def __init__(self, logger: logging.Logger):
super(ArchDiskCacheUpdater, self).__init__(daemon=True)
self.logger = logger
def run(self):
self.logger.info('Pre-caching installed AUR packages data to disk')
installed = pacman.list_and_map_installed()
saved = 0
if installed and installed['not_signed']:
saved = disk.save_several({app for app in installed['not_signed']}, 'aur', overwrite=False)
self.logger.info('Pre-cached data of {} AUR packages to disk'.format(saved))

View File

@@ -0,0 +1,3 @@
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -0,0 +1,2 @@
FLATHUB_URL = 'https://flathub.org'
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'

View File

@@ -0,0 +1,208 @@
from datetime import datetime
from typing import List, Set, Type
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageHistory, PackageUpdate, SoftwarePackage, PackageSuggestion
from bauh.api.abstract.view import MessageType
from bauh.commons.html import strip_html
from bauh.commons.system import SystemProcess, ProcessHandler
from bauh.gems.flatpak import flatpak, suggestions
from bauh.gems.flatpak.model import FlatpakApplication
from bauh.gems.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader
class FlatpakManager(SoftwareManager):
def __init__(self, context: ApplicationContext):
super(FlatpakManager, self).__init__(context=context)
self.i18n = context.i18n
self.api_cache = context.cache_factory.new()
context.disk_loader_factory.map(FlatpakApplication, self.api_cache)
def get_managed_types(self) -> Set["type"]:
return {FlatpakApplication}
def _map_to_model(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
app = FlatpakApplication(arch=app_json.get('arch'),
branch=app_json.get('branch'),
origin=app_json.get('origin'),
runtime=app_json.get('runtime'),
ref=app_json.get('ref'),
commit=app_json.get('commit'),
id=app_json.get('id'),
name=app_json.get('name'),
version=app_json.get('version'),
latest_version=app_json.get('latest_version'))
app.installed = installed
api_data = self.api_cache.get(app_json['id'])
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
if not api_data or expired_data:
if not app_json['runtime']:
if disk_loader:
disk_loader.fill(app) # preloading cached disk data
FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
else:
app.fill_cached_data(api_data)
return app
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
res = SearchResult([], [], 0)
apps_found = flatpak.search(words)
if apps_found:
already_read = set()
installed_apps = self.read_installed(disk_loader=disk_loader).installed
if installed_apps:
for app_found in apps_found:
for installed_app in installed_apps:
if app_found['id'] == installed_app.id:
res.installed.append(installed_app)
already_read.add(app_found['id'])
if len(apps_found) > len(already_read):
for app_found in apps_found:
if app_found['id'] not in already_read:
res.new.append(self._map_to_model(app_found, False, disk_loader))
res.total = len(res.installed) + len(res.new)
return res
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
installed = flatpak.list_installed()
models = []
if installed:
available_updates = flatpak.list_updates_as_str()
for app_json in installed:
model = self._map_to_model(app_json=app_json, installed=True, disk_loader=disk_loader)
model.update = app_json['id'] in available_updates
models.append(model)
return SearchResult(models, None, len(models))
def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
watcher.change_progress(10)
watcher.change_substatus(self.i18n['flatpak.downgrade.commits'])
commits = flatpak.get_app_commits(pkg.ref, pkg.origin)
commit_idx = commits.index(pkg.commit)
# downgrade is not possible if the app current commit in the first one:
if commit_idx == len(commits) - 1:
watcher.show_message(self.i18n['flatpak.downgrade.impossible.title'], self.i18n['flatpak.downgrade.impossible.body'], MessageType.WARNING)
return False
commit = commits[commit_idx + 1]
watcher.change_substatus(self.i18n['flatpak.downgrade.reverting'])
watcher.change_progress(50)
success = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, root_password), success_phrase='Updates complete.'))
watcher.change_progress(100)
return success
def clean_cache_for(self, pkg: FlatpakApplication):
super(FlatpakManager, self).clean_cache_for(pkg)
self.api_cache.delete(pkg.id)
def update(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(pkg.ref)))
def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref)))
def get_info(self, app: FlatpakApplication) -> dict:
app_info = flatpak.get_app_info_fields(app.id, app.branch)
app_info['name'] = app.name
app_info['type'] = 'runtime' if app.runtime else 'app'
app_info['description'] = strip_html(app.description)
return app_info
def get_history(self, pkg: FlatpakApplication) -> PackageHistory:
commits = flatpak.get_app_commits_data(pkg.ref, pkg.origin)
status_idx = 0
for idx, data in enumerate(commits):
if data['commit'] == pkg.commit:
status_idx = idx
break
return PackageHistory(pkg=pkg, history=commits, pkg_status_idx=status_idx)
def install(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.install(pkg.id, pkg.origin)))
def is_enabled(self):
return flatpak.is_installed()
def requires_root(self, action: str, pkg: FlatpakApplication):
return action == 'downgrade'
def prepare(self):
pass
def list_updates(self) -> List[PackageUpdate]:
updates = []
installed = flatpak.list_installed(extra_fields=False)
if installed:
available_updates = flatpak.list_updates_as_str()
if available_updates:
loaders = None
for app_json in installed:
if app_json['id'] in available_updates:
loader = FlatpakUpdateLoader(app=app_json, http_client=self.context.http_client)
loader.start()
if loaders is None:
loaders = []
loaders.append(loader)
if loaders:
for loader in loaders:
loader.join()
app = loader.app
updates.append(PackageUpdate(pkg_id='{}:{}'.format(app['id'], app['branch']),
pkg_type='flatpak',
version=app.get('version')))
return updates
def list_warnings(self) -> List[str]:
if flatpak.is_installed():
if not flatpak.has_remotes_set():
return [self.i18n['flatpak.notification.no_remotes']]
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
res = []
sugs = [(i, p) for i, p in suggestions.ALL.items()]
sugs.sort(key=lambda t: t[1].value, reverse=True)
for sug in sugs:
if limit <= 0 or len(res) < limit:
app_json = flatpak.search(sug[0], app_id=True)
if app_json:
res.append(PackageSuggestion(self._map_to_model(app_json[0], False, None), sug[1]))
else:
break
return res

245
bauh/gems/flatpak/flatpak.py Executable file
View File

@@ -0,0 +1,245 @@
import re
import subprocess
from typing import List
from bauh.api.exception import NoInternetException
from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess
BASE_CMD = 'flatpak'
def app_str_to_json(line: str, version: str, extra_fields: bool = True) -> dict:
app_array = line.split('\t')
if version >= '1.3.0':
app = {'name': app_array[0],
'id': app_array[1],
'version': app_array[2],
'branch': app_array[3]}
elif '1.0' <= version < '1.1':
ref_data = app_array[0].split('/')
app = {'id': ref_data[0],
'arch': ref_data[1],
'name': ref_data[0].split('.')[-1],
'ref': app_array[0],
'options': app_array[1],
'branch': ref_data[2],
'version': None}
elif '1.2' <= version < '1.3':
app = {'id': app_array[1],
'name': app_array[1].strip().split('.')[-1],
'version': app_array[2],
'branch': app_array[3],
'arch': app_array[4],
'origin': app_array[5]}
else:
raise Exception('Unsupported version')
if extra_fields:
extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True)
app.update(extra_fields)
return app
def get_app_info_fields(app_id: str, branch: str, fields: List[str] = [], check_runtime: bool = False):
info = re.findall(r'\w+:\s.+', get_app_info(app_id, branch))
data = {}
fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0)
for field in info:
if fields and fields_to_retrieve == 0:
break
field_val = field.split(':')
field_name = field_val[0].lower()
if not fields or field_name in fields or (check_runtime and field_name == 'ref'):
data[field_name] = field_val[1].strip()
if fields:
fields_to_retrieve -= 1
if check_runtime and field_name == 'ref':
data['runtime'] = data['ref'].startswith('runtime/')
return data
def is_installed():
version = get_version()
return False if version is None else True
def get_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
return res.split(' ')[1].strip() if res else None
def get_app_info(app_id: str, branch: str):
return run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch))
def list_installed(extra_fields: bool = True) -> List[dict]:
apps_str = run_cmd('{} list'.format(BASE_CMD))
if apps_str:
version = get_version()
app_lines = apps_str.split('\n')
return [app_str_to_json(line, version, extra_fields=extra_fields) for line in app_lines if line]
return []
def update(app_ref: str):
"""
Updates the app reference
:param app_ref:
:return:
"""
return new_subprocess([BASE_CMD, 'update', '-y', app_ref])
def uninstall(app_ref: str):
"""
Removes the app by its reference
:param app_ref:
:return:
"""
return new_subprocess([BASE_CMD, 'uninstall', app_ref, '-y'])
def list_updates_as_str():
return run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True)
def downgrade(app_ref: str, commit: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'], root_password)
def get_app_commits(app_ref: str, origin: str) -> List[str]:
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
if log:
return re.findall(r'Commit+:\s(.+)', log)
else:
raise NoInternetException()
def get_app_commits_data(app_ref: str, origin: str) -> List[dict]:
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
if not log:
raise NoInternetException()
res = re.findall(r'(Commit|Subject|Date):\s(.+)', log)
commits = []
commit = {}
for idx, data in enumerate(res):
commit[data[0].strip().lower()] = data[1].strip()
if (idx + 1) % 3 == 0:
commits.append(commit)
commit = {}
return commits
def search(word: str, app_id: bool = False) -> List[dict]:
cli_version = get_version()
res = run_cmd('{} search {}'.format(BASE_CMD, word))
found = []
split_res = res.split('\n')
if split_res and split_res[0].lower() != 'no matches found':
for info in split_res:
if info:
info_list = info.split('\t')
if cli_version >= '1.3.0':
id_ = info_list[2].strip()
if app_id and id_ != word:
continue
version = info_list[3].strip()
app = {
'name': info_list[0].strip(),
'description': info_list[1].strip(),
'id': id_,
'version': version,
'latest_version': version,
'branch': info_list[4].strip(),
'origin': info_list[5].strip(),
'runtime': False,
'arch': None, # unknown at this moment,
'ref': None # unknown at this moment
}
elif cli_version >= '1.2.0':
id_ = info_list[1].strip()
if app_id and id_ != word:
continue
desc = info_list[0].split('-')
version = info_list[2].strip()
app = {
'name': desc[0].strip(),
'description': desc[1].strip(),
'id': id_,
'version': version,
'latest_version': version,
'branch': info_list[3].strip(),
'origin': info_list[4].strip(),
'runtime': False,
'arch': None, # unknown at this moment,
'ref': None # unknown at this moment
}
else:
id_ = info_list[0].strip()
if app_id and id_ != word:
continue
version = info_list[1].strip()
app = {
'name': '',
'description': info_list[4].strip(),
'id': id_,
'version': version,
'latest_version': version,
'branch': info_list[2].strip(),
'origin': info_list[3].strip(),
'runtime': False,
'arch': None, # unknown at this moment,
'ref': None # unknown at this moment
}
found.append(app)
if app_id and len(found) > 0:
break
return found
def install(app_id: str, origin: str):
return new_subprocess([BASE_CMD, 'install', origin, app_id, '-y'])
def set_default_remotes():
run_cmd('{} remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo'.format(BASE_CMD))
def has_remotes_set() -> bool:
return bool(run_cmd('{} remotes'.format(BASE_CMD)).strip())

View File

@@ -0,0 +1,69 @@
from bauh.api.abstract.model import SoftwarePackage
from bauh.commons import resource
from bauh.gems.flatpak import ROOT_DIR
class FlatpakApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None, description: str = None,
branch: str = None, arch: str = None, origin: str = None, runtime: bool = False, ref: str = str, commit: str = str):
super(FlatpakApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description)
self.ref = ref
self.branch = branch
self.arch = arch
self.origin = origin
self.runtime = runtime
self.commit = commit
def is_incomplete(self):
return self.description is None and self.icon_url
def has_history(self):
return self.installed
def has_info(self):
return self.installed
def can_be_downgraded(self):
return self.installed
def get_type(self):
return 'flatpak'
def get_default_icon_path(self):
return resource.get_path('img/flathub.svg', ROOT_DIR)
def get_type_icon_path(self):
return self.get_default_icon_path()
def is_application(self):
return not self.runtime
def get_disk_cache_path(self):
return super(FlatpakApplication, self).get_disk_cache_path() + '/installed/' + self.id
def get_data_to_cache(self):
return {
'description': self.description,
'icon_url': self.icon_url,
'latest_version': self.latest_version,
'version': self.version,
'name': self.name,
'categories': self.categories
}
def fill_cached_data(self, data: dict):
for attr in self.get_data_to_cache().keys():
if data.get(attr) and not getattr(self, attr):
setattr(self, attr, data[attr])
def get_command(self) -> str:
return "flatpak run {}".format(self.id)
def can_be_run(self) -> bool:
return self.installed and not self.runtime
def get_publisher(self):
return 'flathub'

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="47.919052"
height="48.110374"
viewBox="0 0 12.678582 12.729203"
version="1.1"
id="svg3871"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="flathub.svg">
<defs
id="defs3865" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="8.4698372"
inkscape:cy="118.04149"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showborder="false"
inkscape:window-width="1366"
inkscape:window-height="703"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
fit-margin-bottom="-0.2"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
units="px" />
<metadata
id="metadata3868">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-12.122116,-167.33593)">
<g
transform="matrix(0.08875425,0,0,0.11490723,-57.513376,72.576168)"
id="g10674"
style="stroke-width:0.77710575">
<path
sodipodi:nodetypes="ccccccccccc"
inkscape:connector-curvature="0"
id="path10642"
d="m 856.01397,830.22631 -66.76365,43.44891 v 34.93973 l 33.38182,21.72315 33.37926,-21.72315 v -0.005 l 0.003,0.003 33.38183,21.72574 33.37925,-21.72315 v -34.93979 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:9.3252697;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<g
style="stroke-width:0.58728963"
transform="matrix(0.50221552,0,0,0.50636711,429.63205,432.90461)"
id="g10652">
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect10644"
width="79.110413"
height="79.110413"
x="1289.9076"
y="279.42584"
rx="0"
ry="0"
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
id="path10648"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path10650"
d="m 783.00003,824.92983 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<rect
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
ry="0"
rx="0"
y="215.8064"
x="1226.2882"
height="79.110413"
width="79.110413"
id="rect10646"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
<g
style="stroke-width:0.58728963"
transform="matrix(0.50221552,0,0,0.50636711,463.01368,454.62849)"
id="g10662">
<rect
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
ry="0"
rx="0"
y="279.42584"
x="1289.9076"
height="79.110413"
width="79.110413"
id="rect10654"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect10656"
width="79.110413"
height="79.110413"
x="1226.2882"
y="215.8064"
rx="0"
ry="0"
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
<path
inkscape:connector-curvature="0"
id="path10658"
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 783.00003,826.54445 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
id="path10660"
inkscape:connector-curvature="0" />
</g>
<g
style="stroke-width:0.58728963"
id="g10672"
transform="matrix(0.50221552,0,0,0.50636711,396.25042,454.62849)">
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect10664"
width="79.110413"
height="79.110413"
x="1289.9076"
y="279.42584"
rx="0"
ry="0"
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)" />
<rect
transform="matrix(0.84019328,0.54228706,-0.84019328,0.54228706,0,0)"
ry="0"
rx="0"
y="215.8064"
x="1226.2882"
height="79.110413"
width="79.110413"
id="rect10666"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.4608953;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#204a87;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 915.46811,824.92983 -5e-5,68.99997 -66.46803,42.90055 5e-5,-68.99997 z"
id="path10668"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path10670"
d="m 783.00003,824.92983 5e-5,68.99997 66.46803,42.90055 -5e-5,-68.99997 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:2.34915853;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,5 @@
flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps.
flatpak.downgrade.impossible.title=Error
flatpak.downgrade.impossible.body=Impossible to downgrade: the app is in its first version
flatpak.downgrade.commits=Reading package commits
flatpak.downgrade.reverting=Reverting version

View File

@@ -0,0 +1,27 @@
flatpak.info.arch=arquitectura
flatpak.info.branch=rama
flatpak.info.collection=colección
flatpak.info.commit=commit
flatpak.info.date=fecha
flatpak.info.description=descripción
flatpak.info.id=id
flatpak.info.installation=instalación
flatpak.info.installed=instalado
flatpak.info.license=licencia
flatpak.info.name=nombre
flatpak.info.origin=origen
flatpak.info.parent=padre
flatpak.info.ref=ref
flatpak.info.runtime=runtime
flatpak.info.sdk=sdk
flatpak.info.subject=tema
flatpak.info.type=tipo
flatpak.info.version=versión
flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak.
flatpak.downgrade.impossible.title=Error
flatpak.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión
flatpak.history.date=fecha
flatpak.history.commit=commit
flatpak.history.subject=tema
flatpak.downgrade.commits=Leyendo commits del paquete
flatpak.downgrade.reverting=Revirtiendo la versión

View File

@@ -0,0 +1,27 @@
flatpak.info.arch=arquitetura
flatpak.info.branch=ramo
flatpak.info.collection=coleção
flatpak.info.commit=commit
flatpak.info.date=data
flatpak.info.description=descrição
flatpak.info.id=id
flatpak.info.installation=instalação
flatpak.info.installed=instalado
flatpak.info.license=licença
flatpak.info.name=nome
flatpak.info.origin=origem
flatpak.info.parent=pai
flatpak.info.ref=ref
flatpak.info.runtime=runtime
flatpak.info.sdk=sdk
flatpak.info.subject=assunto
flatpak.info.type=tipo
flatpak.info.version=versão
flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak.
flatpak.downgrade.impossible.title=Erro
flatpak.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão
flatpak.history.date=data
flatpak.history.commit=commit
flatpak.history.subject=assunto
flatpak.downgrade.commits=Lendo os commits do pacote
flatpak.downgrade.reverting=Revertendo a versão

View File

@@ -0,0 +1,15 @@
from bauh.api.abstract.model import SuggestionPriority
ALL = {
'com.spotify.Client': SuggestionPriority.TOP,
'com.skype.Client': SuggestionPriority.HIGH,
'com.dropbox.Client': SuggestionPriority.MEDIUM,
'us.zoom.Zoom': SuggestionPriority.MEDIUM,
'org.telegram.desktop': SuggestionPriority.MEDIUM,
'com.visualstudio.code': SuggestionPriority.LOW,
'org.inkscape.Inkscape': SuggestionPriority.LOW,
'org.libretro.RetroArch': SuggestionPriority.LOW,
'org.kde.kdenlive': SuggestionPriority.LOW,
'org.videolan.VLC': SuggestionPriority.LOW
}

View File

@@ -0,0 +1,96 @@
import traceback
from threading import Thread
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import PackageStatus
from bauh.api.http import HttpClient
from bauh.gems.flatpak import flatpak
from bauh.gems.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL
from bauh.gems.flatpak.model import FlatpakApplication
class FlatpakAsyncDataLoader(Thread):
def __init__(self, app: FlatpakApplication, manager: SoftwareManager, context: ApplicationContext, api_cache: MemoryCache):
super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
self.app = app
self.manager = manager
self.http_client = context.http_client
self.api_cache = api_cache
self.persist = False
self.logger = context.logger
def run(self):
if self.app:
self.app.status = PackageStatus.LOADING_DATA
try:
res = self.http_client.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app.id))
if res.status_code == 200 and res.text:
data = res.json()
if not self.app.version:
self.app.version = data.get('version')
if not self.app.name:
self.app.name = data.get('name')
self.app.description = data.get('description', data.get('summary', None))
self.app.icon_url = data.get('iconMobileUrl', None)
self.app.latest_version = data.get('currentReleaseVersion', self.app.version)
if not self.app.version and self.app.latest_version:
self.app.version = self.app.latest_version
if not self.app.installed and self.app.latest_version:
self.app.version = self.app.latest_version
if self.app.icon_url and self.app.icon_url.startswith('/'):
self.app.icon_url = FLATHUB_URL + self.app.icon_url
if data.get('categories'):
self.app.categories = [c['name'] for c in data['categories']]
loaded_data = self.app.get_data_to_cache()
self.api_cache.add(self.app.id, loaded_data)
self.persist = self.app.supports_disk_cache()
else:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
except:
self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id))
traceback.print_exc()
self.app.status = PackageStatus.READY
if self.persist:
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)
class FlatpakUpdateLoader(Thread):
def __init__(self, app: dict, http_client: HttpClient):
super(FlatpakUpdateLoader, self).__init__(daemon=True)
self.app = app
self.http_client = http_client
def run(self):
if self.app.get('ref') is None:
self.app.update(flatpak.get_app_info_fields(self.app['id'], self.app['branch'], fields=['ref'], check_runtime=True))
else:
self.app['runtime'] = self.app['ref'].startswith('runtime/')
if not self.app['runtime']:
try:
data = self.http_client.get_json('{}/apps/{}'.format(FLATHUB_API_URL, self.app['id']))
if data and data.get('currentReleaseVersion'):
self.app['version'] = data['currentReleaseVersion']
except:
traceback.print_exc()

View File

@@ -0,0 +1,2 @@
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -0,0 +1 @@
SNAP_API_URL = 'https://search.apps.ubuntu.com/api/v1'

View File

@@ -0,0 +1,145 @@
from datetime import datetime
from typing import List, Set, Type
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion
from bauh.commons.system import SystemProcess, ProcessHandler
from bauh.gems.snap import snap, suggestions
from bauh.gems.snap.model import SnapApplication
from bauh.gems.snap.worker import SnapAsyncDataLoader
class SnapManager(SoftwareManager):
def __init__(self, context: ApplicationContext):
super(SnapManager, self).__init__(context=context)
self.i18n = context.i18n
self.api_cache = context.cache_factory.new()
context.disk_loader_factory.map(SnapApplication, self.api_cache)
def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> SnapApplication:
app = SnapApplication(publisher=app_json.get('publisher'),
rev=app_json.get('rev'),
notes=app_json.get('notes'),
app_type=app_json.get('type'),
id=app_json.get('name'),
name=app_json.get('name'),
version=app_json.get('version'),
latest_version=app_json.get('version'),
description=app_json.get('description'))
if app.publisher:
app.publisher = app.publisher.replace('*', '')
app.installed = installed
api_data = self.api_cache.get(app_json['name'])
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
if (not api_data or expired_data) and app.is_application():
if disk_loader and app.installed:
disk_loader.fill(app)
SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
else:
app.fill_cached_data(api_data)
return app
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
installed = self.read_installed(disk_loader).installed
res = SearchResult([], [], 0)
for app_json in snap.search(words):
already_installed = None
if installed:
already_installed = [i for i in installed if i.id == app_json.get('name')]
already_installed = already_installed[0] if already_installed else None
if already_installed:
res.installed.append(already_installed)
else:
res.new.append(self.map_json(app_json, installed=False, disk_loader=disk_loader))
res.total = len(res.installed) + len(res.new)
return res
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader) for app_json in snap.read_installed()]
return SearchResult(installed, None, len(installed))
def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.downgrade_and_stream(pkg.name, root_password), wrong_error_phrase=None))
def update(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> SystemProcess:
raise Exception("'update' is not supported by {}".format(pkg.__class__.__name__))
def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.uninstall_and_stream(pkg.name, root_password)))
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
return {SnapApplication}
def clean_cache_for(self, pkg: SnapApplication):
super(SnapManager, self).clean_cache_for(pkg)
self.api_cache.delete(pkg.id)
def get_info(self, pkg: SnapApplication) -> dict:
info = snap.get_info(pkg.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed'))
info['description'] = pkg.description
info['publisher'] = pkg.publisher
info['revision'] = pkg.rev
info['name'] = pkg.name
if info.get('commands'):
info['commands'] = ' '.join(info['commands'])
return info
def get_history(self, pkg: SnapApplication) -> PackageHistory:
raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__))
def install(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.install_and_stream(pkg.name, pkg.confinement, root_password)))
def is_enabled(self) -> bool:
return snap.is_installed()
def requires_root(self, action: str, pkg: SnapApplication):
return action != 'search'
def refresh(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password)))
def prepare(self):
pass
def list_updates(self) -> List[PackageUpdate]:
pass
def list_warnings(self) -> List[str]:
if snap.get_snapd_version() == 'unavailable':
return [self.i18n['snap.notification.snapd_unavailable']]
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
res = []
sugs = [(i, p) for i, p in suggestions.ALL.items()]
sugs.sort(key=lambda t: t[1].value, reverse=True)
for sug in sugs:
if limit <= 0 or len(res) < limit:
found = snap.search(sug[0], exact_name=True)
if found:
res.append(PackageSuggestion(self.map_json(found[0], installed=False, disk_loader=None), sug[1]))
else:
break
return res

88
bauh/gems/snap/model.py Normal file
View File

@@ -0,0 +1,88 @@
from typing import List
from bauh.api.abstract.model import SoftwarePackage, PackageAction
from bauh.commons import resource
from bauh.gems.snap import ROOT_DIR
EXTRA_INSTALLED_ACTIONS = [
PackageAction(i18n_status_key='snap.action.refresh.status',
i18_label_key='snap.action.refresh.label',
icon_path=resource.get_path('img/refresh.svg', ROOT_DIR),
manager_method='refresh',
requires_root=True)
]
class SnapApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None,
description: str = None, publisher: str = None, rev: str = None, notes: str = None,
app_type: str = None, confinement: str = None):
super(SnapApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description)
self.publisher = publisher
self.rev = rev
self.notes = notes
self.type = app_type
self.confinement = confinement
def has_history(self):
return False
def has_info(self):
return True
def can_be_downgraded(self):
return self.installed
def get_type(self):
return 'snap'
def get_default_icon_path(self):
return resource.get_path('img/snap.png', ROOT_DIR)
def get_type_icon_path(self):
return self.get_default_icon_path()
def is_application(self):
return not self.type and (self.type not in ('core', 'base', 'snapd') and not self._name_starts_with(('gtk-', 'gnome-', 'kde-', 'gtk2-')))
def _name_starts_with(self, words: tuple):
for word in words:
if self.name.startswith(word):
return True
return False
def get_disk_cache_path(self):
return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name
def get_data_to_cache(self):
return {
"icon_url": self.icon_url,
'confinement': self.confinement,
'description': self.description
}
def fill_cached_data(self, data: dict):
if data:
for base_attr in ('icon_url', 'description'):
if data.get(base_attr):
setattr(self, base_attr, data[base_attr])
if data.get('confinement'):
self.confinement = data['confinement']
def get_command(self) -> str:
return "snap run " + self.name
def can_be_run(self) -> bool:
return self.installed and self.is_application()
def get_publisher(self):
return self.publisher
def get_custom_supported_actions(self) -> List[PackageAction]:
if self.installed:
return EXTRA_INSTALLED_ACTIONS

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="48"
height="48"
viewBox="0 0 47.999999 47.999999"
xml:space="preserve"
sodipodi:docname="refresh_2.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata45"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs43"><linearGradient
inkscape:collect="always"
id="linearGradient540"><stop
style="stop-color:#0088aa;stop-opacity:1;"
offset="0"
id="stop536" /><stop
style="stop-color:#0088aa;stop-opacity:0;"
offset="1"
id="stop538" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient540"
id="linearGradient542"
x1="-130.74721"
y1="-214.80638"
x2="-556.49438"
y2="110.01643"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview41"
showgrid="false"
showborder="false"
inkscape:zoom="1.9374833"
inkscape:cx="165.58246"
inkscape:cy="-10.288149"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="g8"
transform="matrix(0.10547014,0,0,0.0985161,-1.6940777,0)"
style="fill:url(#linearGradient542);fill-opacity:1">
<g
id="g6"
style="fill:url(#linearGradient542);fill-opacity:1">
<path
d="m 55.323,203.641 c 15.664,0 29.813,-9.405 35.872,-23.854 25.017,-59.604 83.842,-101.61 152.42,-101.61 37.797,0 72.449,12.955 100.23,34.442 l -21.775,3.371 c -7.438,1.153 -13.224,7.054 -14.232,14.512 -1.01,7.454 3.008,14.686 9.867,17.768 l 119.746,53.872 c 5.249,2.357 11.33,1.904 16.168,-1.205 4.83,-3.114 7.764,-8.458 7.796,-14.208 l 0.621,-131.943 c 0.042,-7.506 -4.851,-14.144 -12.024,-16.332 -7.185,-2.188 -14.947,0.589 -19.104,6.837 L 414.403,70.096 C 370.398,26.778 310.1,0 243.615,0 142.806,0 56.133,61.562 19.167,149.06 c -5.134,12.128 -3.84,26.015 3.429,36.987 7.269,10.976 19.556,17.594 32.727,17.594 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:url(#linearGradient542);fill-opacity:1" />
<path
d="m 464.635,301.184 c -7.27,-10.977 -19.558,-17.594 -32.728,-17.594 -15.664,0 -29.813,9.405 -35.872,23.854 -25.018,59.604 -83.843,101.61 -152.42,101.61 -37.798,0 -72.45,-12.955 -100.232,-34.442 l 21.776,-3.369 c 7.437,-1.153 13.223,-7.055 14.233,-14.514 1.009,-7.453 -3.008,-14.686 -9.867,-17.768 L 49.779,285.089 c -5.25,-2.356 -11.33,-1.905 -16.169,1.205 -4.829,3.114 -7.764,8.458 -7.795,14.207 l -0.622,131.943 c -0.042,7.506 4.85,14.144 12.024,16.332 7.185,2.188 14.948,-0.59 19.104,-6.839 l 16.505,-24.805 c 44.004,43.32 104.303,70.098 170.788,70.098 100.811,0 187.481,-61.561 224.446,-149.059 5.137,-12.128 3.843,-26.014 -3.425,-36.987 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:url(#linearGradient542);fill-opacity:1" />
</g>
</g>
<g
id="g10"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g12"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g14"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g16"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g18"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g20"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g22"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g24"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g26"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g28"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g30"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g32"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g34"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g36"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g38"
transform="translate(-16.062155,-439.23)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
snap.notification.snapd_unavailable=snapd seems not to be enabled. snap packages will not be available.
snap.action.refresh.status=Refreshing
snap.action.refresh.label=Refresh

View File

@@ -0,0 +1,12 @@
snap.info.commands=comandos
snap.info.contact=contacto
snap.info.description=descripción
snap.info.license=licencia
snap.info.license.unset=no está definida
snap.info.revision=revisión
snap.info.tracking=tracking
snap.info.installed=instalado
snap.info.publisher=publicador
snap.notification.snapd_unavailable=snapd no parece estar habilitado. los paquetes snap estarán indisponibles.
snap.action.refresh.status=Actualizando
snap.action.refresh.label=Actualizar

View File

@@ -0,0 +1,12 @@
snap.info.commands=comandos
snap.info.contact=contato
snap.info.description=descrição
snap.info.license=licença
snap.info.license.unset=não definida
snap.info.revision=revisão
snap.info.tracking=tracking
snap.info.installed=instalado
snap.info.publisher=publicador
snap.notification.snapd_unavailable=snapd não parece estar habilitado. os pacotes snap estarão indisponíveis.
snap.action.refresh.status=Atualizando
snap.action.refresh.label=Atualizar

141
bauh/gems/snap/snap.py Normal file
View File

@@ -0,0 +1,141 @@
import re
import subprocess
from typing import List
from bauh.commons.system import new_root_subprocess, run_cmd
BASE_CMD = 'snap'
def is_installed():
version = get_snapd_version()
return False if version is None else True
def get_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
return res.split('\n')[0].split(' ')[-1].strip() if res else None
def get_snapd_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
if not res:
return None
else:
lines = res.split('\n')
if lines and len(lines) >= 2:
version = lines[1].split(' ')[-1].strip()
return version if version and version.lower() != 'unavailable' else None
else:
return None
def app_str_to_json(app: str) -> dict:
app_data = [word for word in app.split(' ') if word]
app_json = {
'name': app_data[0],
'version': app_data[1],
'rev': app_data[2],
'tracking': app_data[3],
'publisher': app_data[4] if len(app_data) >= 5 else None,
'notes': app_data[5] if len(app_data) >= 6 else None
}
app_json.update(get_info(app_json['name'], ('summary', 'type', 'description')))
return app_json
def get_info(app_name: str, attrs: tuple = None):
full_info_lines = run_cmd('{} info {}'.format(BASE_CMD, app_name))
data = {}
if full_info_lines:
re_attrs = r'\w+' if not attrs else '|'.join(attrs)
info_map = re.findall(r'({}):\s+(.+)'.format(re_attrs), full_info_lines)
for info in info_map:
data[info[0]] = info[1].strip()
if not attrs or 'description' in attrs:
desc = re.findall(r'\|\n+((\s+.+\n+)+)', full_info_lines)
data['description'] = ''.join([w.strip() for w in desc[0][0].strip().split('\n')]).replace('.', '.\n') if desc else None
if not attrs or 'commands' in attrs:
commands = re.findall(r'commands:\s*\n*((\s+-\s.+\s*\n)+)', full_info_lines)
data['commands'] = commands[0][0].replace('-', '').strip().split('\n') if commands else None
return data
def read_installed() -> List[dict]:
res = run_cmd('{} list'.format(BASE_CMD), print_error=False)
apps = []
if res and len(res) > 0:
lines = res.split('\n')
if not lines[0].startswith('error'):
for idx, app_str in enumerate(lines):
if idx > 0 and app_str:
apps.append(app_str_to_json(app_str))
return apps
def search(word: str, exact_name: bool = False) -> List[dict]:
apps = []
res = run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False)
if res:
res = res.split('\n')
if not res[0].startswith('No matching'):
for idx, app_str in enumerate(res):
if idx > 0 and app_str:
app_data = [word for word in app_str.split(' ') if word]
if exact_name and app_data[0] != word:
continue
apps.append({
'name': app_data[0],
'version': app_data[1],
'publisher': app_data[2],
'notes': app_data[3] if app_data[3] != '-' else None,
'summary': app_data[4] if len(app_data) == 5 else '',
'rev': None,
'tracking': None,
'type': None
})
if exact_name and len(apps) > 0:
break
return apps
def uninstall_and_stream(app_name: str, root_password: str):
return new_root_subprocess([BASE_CMD, 'remove', app_name], root_password)
def install_and_stream(app_name: str, confinement: str, root_password: str) -> subprocess.Popen:
install_cmd = [BASE_CMD, 'install', app_name] # default
if confinement == 'classic':
install_cmd.append('--classic')
return new_root_subprocess(install_cmd, root_password)
def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'revert', app_name], root_password)
def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'refresh', app_name], root_password)

View File

@@ -0,0 +1,13 @@
from bauh.api.abstract.model import SuggestionPriority
ALL = {
'whatsdesk': SuggestionPriority.TOP,
'slack': SuggestionPriority.HIGH,
'discord': SuggestionPriority.HIGH,
'yakyak': SuggestionPriority.MEDIUM,
'instagraph': SuggestionPriority.MEDIUM,
'gimp': SuggestionPriority.MEDIUM,
'pycharm-professional': SuggestionPriority.LOW,
'eclipse': SuggestionPriority.LOW,
'supertuxkart': SuggestionPriority.LOW
}

71
bauh/gems/snap/worker.py Normal file
View File

@@ -0,0 +1,71 @@
import traceback
from threading import Thread
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import PackageStatus
from bauh.gems.snap import snap
from bauh.gems.snap.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication
class SnapAsyncDataLoader(Thread):
def __init__(self, app: SnapApplication, manager: SoftwareManager, api_cache: MemoryCache,
context: ApplicationContext):
super(SnapAsyncDataLoader, self).__init__(daemon=True)
self.app = app
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
self.manager = manager
self.http_client = context.http_client
self.api_cache = api_cache
self.persist = False
self.download_icons = context.download_icons
self.logger = context.logger
def run(self):
if self.app:
self.app.status = PackageStatus.LOADING_DATA
try:
res = self.http_client.session.get('{}/search?q={}'.format(SNAP_API_URL, self.app.name))
if res:
try:
snap_list = res.json()['_embedded']['clickindex:package']
except:
self.logger.warning('Snap API response responded differently from expected for app: {}'.format(self.app.name))
return
if not snap_list:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
else:
snap_data = snap_list[0]
api_data = {
'confinement': snap_data.get('confinement'),
'description': snap_data.get('description'),
'icon_url': snap_data.get('icon_url') if self.download_icons else None
}
self.api_cache.add(self.app.id, api_data)
self.app.confinement = api_data['confinement']
self.app.icon_url = api_data['icon_url']
if not api_data.get('description'):
api_data['description'] = snap.get_info(self.app.name, ('description',)).get('description')
self.app.description = api_data['description']
self.persist = self.app.supports_disk_cache()
else:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
except:
self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id))
traceback.print_exc()
self.app.status = PackageStatus.READY
if self.persist:
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)

View File

@@ -2,7 +2,7 @@ import datetime
import time
from threading import Lock, Thread
from bauh_api.abstract.cache import MemoryCache, MemoryCacheFactory
from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory
class DefaultMemoryCache(MemoryCache):

View File

@@ -3,9 +3,9 @@ import os
from threading import Thread, Lock
from typing import Type, Dict
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory
from bauh_api.abstract.model import SoftwarePackage
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory
from bauh.api.abstract.model import SoftwarePackage
class AsyncDiskCacheLoader(Thread, DiskCacheLoader):

View File

@@ -7,8 +7,8 @@ from PyQt5.QtGui import QPixmap, QIcon, QCursor
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.model import PackageStatus
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.model import PackageStatus
from bauh.util import resource
from bauh.util.html import strip_html

View File

@@ -1,7 +1,10 @@
from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \
QLabel, QSizePolicy, QLineEdit
from bauh_api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType
from bauh import ROOT_DIR
from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType
from bauh.commons import resource
class RadioButtonQt(QRadioButton):
@@ -19,19 +22,30 @@ class RadioButtonQt(QRadioButton):
class CheckboxQt(QCheckBox):
def __init__(self, model: InputOption, model_parent: MultipleSelectComponent):
def __init__(self, model: InputOption, model_parent: MultipleSelectComponent, callback):
super(CheckboxQt, self).__init__()
self.model = model
self.model_parent = model_parent
self.stateChanged.connect(self._set_checked)
self.callback = callback
self.setText(model.label)
self.setToolTip(model.tooltip)
if model.icon_path:
self.setIcon(QIcon(model.icon_path))
def _set_checked(self, state):
if state == 2:
checked = state == 2
if checked:
self.model_parent.values.add(self.model)
else:
if self.model in self.model_parent.values:
self.model_parent.values.remove(self.model)
if self.callback:
self.callback(self.model, checked)
class ComboBoxQt(QComboBox):
@@ -101,8 +115,8 @@ class ComboSelectQt(QGroupBox):
class MultipleSelectQt(QGroupBox):
def __init__(self, model: MultipleSelectComponent):
super(MultipleSelectQt, self).__init__(model.label + ' :' if model.label else None)
def __init__(self, model: MultipleSelectComponent, callback):
super(MultipleSelectQt, self).__init__(model.label if model.label else None)
self.setStyleSheet("QGroupBox { font-weight: bold }")
self.model = model
self._layout = QGridLayout()
@@ -110,9 +124,7 @@ class MultipleSelectQt(QGroupBox):
line, col = 0, 0
for op in model.options:
comp = CheckboxQt(op, model)
comp.setText(op.label)
comp.setToolTip(op.tooltip)
comp = CheckboxQt(op, model, callback)
if model.values and op in model.values:
self.value = comp

View File

@@ -1,7 +1,7 @@
from typing import List
from PyQt5.QtWidgets import QMessageBox, QVBoxLayout, QLabel, QWidget
from bauh_api.abstract.view import ViewComponent, SingleSelectComponent, MultipleSelectComponent
from bauh.api.abstract.view import ViewComponent, SingleSelectComponent, MultipleSelectComponent
from bauh.view.qt.components import MultipleSelectQt, new_single_select
@@ -29,7 +29,7 @@ class ConfirmationDialog(QMessageBox):
if isinstance(comp, SingleSelectComponent):
inst = new_single_select(comp)
elif isinstance(comp, MultipleSelectComponent):
inst = MultipleSelectQt(comp)
inst = MultipleSelectQt(comp, None)
else:
raise Exception("Cannot render instances of " + comp.__class__.__name__)

View File

@@ -2,7 +2,7 @@ from typing import List
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QMessageBox, QLabel, QWidget, QHBoxLayout
from bauh_api.abstract.view import MessageType
from bauh.api.abstract.view import MessageType
from bauh.util import resource

View File

@@ -0,0 +1,69 @@
import subprocess
import sys
from typing import List
from PyQt5.QtCore import Qt, QCoreApplication
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QLabel, QGridLayout, QPushButton
from bauh import ROOT_DIR
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.view import MultipleSelectComponent, InputOption
from bauh.commons import resource
from bauh.core.config import Configuration, save
from bauh.view.qt.components import MultipleSelectQt, CheckboxQt, new_spacer
class GemSelectorPanel(QWidget):
def __init__(self, managers: List[SoftwareManager], i18n: dict, boot: bool):
super(GemSelectorPanel, self).__init__()
self.managers = managers
self.setLayout(QGridLayout())
self.setWindowIcon(QIcon(resource.get_path('img/logo.svg', ROOT_DIR)))
self.setWindowTitle('Welcome' if boot else 'Supported types')
self.resize(400, 400)
self.label_question = QLabel('What types of applications do you want to find here ?')
self.label_question.setStyleSheet('QLabel { font-weight: bold}')
self.layout().addWidget(self.label_question, 0, 1, Qt.AlignHCenter)
self.bt_proceed = QPushButton('Proceed')
self.bt_proceed.setStyleSheet('QPushButton { background: green; color: white; font-weight: bold}')
self.bt_proceed.setEnabled(True)
self.bt_proceed.clicked.connect(self.save)
self.bt_exit = QPushButton('Exit')
self.bt_exit.setStyleSheet('QPushButton { background: red; color: white; font-weight: bold}')
self.bt_exit.clicked.connect(lambda: QCoreApplication.exit())
gem_options = []
for m in managers:
modname = m.__module__.split('.')[-2]
gem_options.append(InputOption(label=i18n.get('gem.{}.label'.format(modname), modname.capitalize()),
value=modname,
icon_path='{r}/gems/{n}/resources/img/{n}.png'.format(r=ROOT_DIR, n=modname)))
self.gem_select_model = MultipleSelectComponent(label='', options=gem_options, default_options=set(gem_options), max_per_line=3)
self.gem_select = MultipleSelectQt(self.gem_select_model, self.check_state)
self.layout().addWidget(self.gem_select, 1, 1)
self.layout().addWidget(new_spacer(), 2, 1)
self.layout().addWidget(self.bt_proceed, 3, 1, Qt.AlignRight)
self.layout().addWidget(self.bt_exit, 3, 1, Qt.AlignLeft)
self.adjustSize()
self.setFixedSize(self.size())
def check_state(self, model: CheckboxQt, checked: bool):
self.bt_proceed.setEnabled(bool(self.gem_select_model.values))
def save(self):
config = Configuration(gems=[o.value for o in self.gem_select_model.values])
save(config)
subprocess.Popen([sys.executable, *sys.argv])
self.close()

View File

@@ -4,8 +4,8 @@ from functools import reduce
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.model import PackageHistory
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.model import PackageHistory
class HistoryDialog(QDialog):

View File

@@ -2,7 +2,7 @@ from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QToolBar
from bauh_api.abstract.cache import MemoryCache
from bauh.api.abstract.cache import MemoryCache
from bauh.util.html import strip_html

View File

@@ -3,7 +3,7 @@ import os
import subprocess
from PyQt5.QtWidgets import QInputDialog, QLineEdit
from bauh_api.abstract.view import MessageType
from bauh.api.abstract.view import MessageType
from bauh.view.qt.dialog import show_message

View File

@@ -5,10 +5,10 @@ from typing import List
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from bauh_api.abstract.model import PackageUpdate
from bauh.api.abstract.model import PackageUpdate
from bauh import __app_name__
from bauh_api.abstract.controller import SoftwareManager
from bauh.api.abstract.controller import SoftwareManager
from bauh.util import util, resource
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.window import ManageWindow

View File

@@ -5,12 +5,12 @@ from typing import List, Type, Set
import requests
from PyQt5.QtCore import QThread, pyqtSignal
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.controller import SoftwareManager
from bauh_api.abstract.handler import ProcessWatcher
from bauh_api.abstract.model import PackageStatus, SoftwarePackage, PackageAction
from bauh_api.abstract.view import InputViewComponent, MessageType
from bauh_api.exception import NoInternetException
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageStatus, SoftwarePackage, PackageAction
from bauh.api.abstract.view import InputViewComponent, MessageType
from bauh.api.exception import NoInternetException
from bauh.view.qt import commons
from bauh.view.qt.view_model import PackageView

View File

@@ -1,6 +1,6 @@
from enum import Enum
from bauh_api.abstract.model import SoftwarePackage
from bauh.api.abstract.model import SoftwarePackage
class PackageViewStatus(Enum):

View File

@@ -6,10 +6,10 @@ from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.controller import SoftwareManager
from bauh_api.abstract.model import SoftwarePackage, PackageAction
from bauh_api.abstract.view import MessageType
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import SoftwarePackage, PackageAction
from bauh.api.abstract.view import MessageType
from bauh.util import util, resource
from bauh.view.qt import dialog, commons

View File

@@ -1,2 +1,3 @@
pyqt5>=5.12
bauh_api>=0.1.0<0.2
requests>=2.22
colorama>=0.4.1