This commit is contained in:
Vinícius Moreira
2020-04-13 11:49:28 -03:00
parent 83aab1ff55
commit 01a60ea686
165 changed files with 12778 additions and 6171 deletions

View File

@@ -10,11 +10,10 @@ from bauh.view.util.translation import I18n
class ApplicationContext:
def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n,
def __init__(self, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n,
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str):
"""
: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
@@ -25,8 +24,8 @@ class ApplicationContext:
:param file_downloader
:param distro
:param app_name
:param root_password
"""
self.disk_cache = disk_cache
self.download_icons = download_icons
self.http_client = http_client
self.app_root_dir = app_root_dir
@@ -40,6 +39,7 @@ class ApplicationContext:
self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game',
'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility')
self.app_name = app_name
self.root_password = None
def is_system_x86_64(self):
return self.arch_x86_64

View File

@@ -9,8 +9,9 @@ import yaml
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
from bauh.api.abstract.handler import ProcessWatcher, TaskManager
from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \
CustomSoftwareAction
from bauh.api.abstract.view import ViewComponent
@@ -27,6 +28,38 @@ class SearchResult:
self.total = total
class UpgradeRequirement:
def __init__(self, pkg: SoftwarePackage, reason: str = None, required_size: int = None, extra_size: int = None):
"""
:param pkg:
:param reason:
:param required_size: size in BYTES required to upgrade the package
:param extra_size: the extra size IN BYTES the upgrade will allocate in relation to the already allocated
"""
self.pkg = pkg
self.reason = reason
self.required_size = required_size
self.extra_size = extra_size
class UpgradeRequirements:
def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[UpgradeRequirement],
to_upgrade: List[UpgradeRequirement], cannot_upgrade: List[UpgradeRequirement]):
"""
:param to_install: additional packages that must be installed with the upgrade
:param to_remove: non upgrading packages that should be removed due to conflicts with upgrading packages
:param to_upgrade: the final packages to update
:param cannot_upgrade: packages which conflict with each other
"""
self.to_install = to_install
self.to_remove = to_remove # when an upgrading package conflicts with a not upgrading package ( check all the non-upgrading packages deps an add here [including those selected to upgrade as well]
self.to_upgrade = to_upgrade
self.cannot_upgrade = cannot_upgrade
class SoftwareManager(ABC):
"""
@@ -82,27 +115,19 @@ class SoftwareManager(ABC):
if pkg.supports_disk_cache() and os.path.exists(pkg.get_disk_cache_path()):
shutil.rmtree(pkg.get_disk_cache_path())
def sort_update_order(self, pkgs: List[SoftwarePackage]) -> List[SoftwarePackage]:
def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: str, watcher: ProcessWatcher) -> UpgradeRequirements:
"""
sorts the best order to perform the update of some packages
:param pkgs:
:return:
"""
return pkgs
def get_update_requirements(self, pkgs: List[SoftwarePackage], watcher: ProcessWatcher) -> List[SoftwarePackage]:
"""
return additional required software that needs to be installed before updating a list of packages
return additional required software that needs to be installed / removed / updated before updating a list of packages
:param pkgs:
:param watcher
:return:
"""
return []
return UpgradeRequirements(None, None, [UpgradeRequirement(p) for p in pkgs], None)
@abstractmethod
def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool:
"""
:param pkg:
:param requirements:
:param root_password: the root user password (if required)
:param watcher:
:return:
@@ -182,7 +207,7 @@ class SoftwareManager(ABC):
:param only_icon: if only the icon should be saved
:return:
"""
if self.context.disk_cache and pkg.supports_disk_cache():
if pkg.supports_disk_cache():
self.serialize_to_disk(pkg, icon_bytes, only_icon)
def serialize_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
@@ -217,7 +242,7 @@ class SoftwareManager(ABC):
@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'
if a given action requires root privileges to be executed. Current actions are: 'install', 'uninstall', 'downgrade', 'search', 'refresh', 'prepare'
:param action:
:param pkg:
:return:
@@ -225,9 +250,11 @@ class SoftwareManager(ABC):
pass
@abstractmethod
def prepare(self):
def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool):
"""
It prepares the manager to start working. It will be called by GUI. Do not call it within.
:param task_manager: a task manager instance used to register ongoing tasks during prepare
:param root_password
:return:
"""
pass
@@ -257,7 +284,7 @@ class SoftwareManager(ABC):
"""
pass
def execute_custom_action(self, action: PackageAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool:
"""
At the moment the GUI implements this action. No need to implement it yourself.
:param action:
@@ -304,3 +331,12 @@ class SoftwareManager(ABC):
:return: a tuple with a bool informing if the settings were saved and a list of error messages
"""
pass
def get_custom_actions(self) -> List[CustomSoftwareAction]:
"""
:return: custom actions
"""
pass
def fill_sizes(self, pkgs: List[SoftwarePackage]):
pass

View File

@@ -17,7 +17,7 @@ class ProcessWatcher:
"""
pass
def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None)-> bool:
def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None, deny_button: bool = True)-> bool:
"""
request a user confirmation. In the current GUI implementation, it shows a popup to the user.
:param title: popup title
@@ -25,10 +25,17 @@ class ProcessWatcher:
: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')
:param deny_button: if the deny button should be displayed
:return: if the request was confirmed by the user
"""
pass
def request_reboot(self, msg: str) -> bool:
"""
:return: requests a system reboot
"""
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.
@@ -70,4 +77,33 @@ class ProcessWatcher:
"""
asks the root password for the user
:return: a tuple with the typed password and if it is valid
"""
"""
class TaskManager:
def register_task(self, id_: str, label: str, icon_path: str):
"""
:param id_: an unique identifier for the task
:param label: an i18n label
:param icon_path: str
:return:
"""
pass
def update_progress(self, task_id: str, progress: float, substatus: str):
"""
:param task_id:
:param progress: a float between 0 and 100.
:param substatus: a substatus string representing the current state
:return:
"""
pass
def finish_task(self, task_id: str):
"""
marks a task as finished
:param task_id:
:return:
"""
pass

View File

@@ -5,22 +5,31 @@ from typing import List
from bauh.api.constants import CACHE_PATH
class PackageAction:
class CustomSoftwareAction:
def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, requires_root: bool):
def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, requires_root: bool, manager: "SoftwareManager" = None, backup: bool = False):
"""
: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 manager: the instance that will execute the action ( optional )
:param backup: if a system backup should be performed before executing the action
: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
self.manager = manager
self.backup = backup
def __hash__(self):
return self.i18_label_key.__hash__() + self.i18n_status_key.__hash__() + self.manager_method.__hash__()
def __repr__(self):
return "CustomAction (label={}, method={})".format(self.i18_label_key, self.manager_method)
class PackageStatus(Enum):
@@ -58,6 +67,7 @@ class SoftwarePackage(ABC):
self.size = size
self.categories = categories
self.license = license
self.gem_name = self.__module__.split('.')[2]
@abstractmethod
def has_history(self):
@@ -122,11 +132,21 @@ class SoftwarePackage(ABC):
"""
return CACHE_PATH + '/' + self.get_type()
def can_be_updated(self) -> bool:
"""
:return: if the package can be updated.
"""
return self.installed and self.update
def get_disk_icon_path(self):
return '{}/icon.png'.format(self.get_disk_cache_path())
path = self.get_disk_cache_path()
if path:
return '{}/icon.png'.format(path)
def get_disk_data_path(self):
return '{}/data.json'.format(self.get_disk_cache_path())
path = self.get_disk_cache_path()
if path:
return '{}/data.json'.format(path)
@abstractmethod
def get_data_to_cache(self) -> dict:
@@ -163,7 +183,7 @@ class SoftwarePackage(ABC):
"""
pass
def get_custom_supported_actions(self) -> List[PackageAction]:
def get_custom_supported_actions(self) -> List[CustomSoftwareAction]:
"""
:return: custom supported actions
"""
@@ -181,24 +201,35 @@ class SoftwarePackage(ABC):
"""
return self.name
def get_display_name(self) -> str:
"""
:return: name displayed on the table
"""
return self.name
@abstractmethod
def supports_backup(self) -> bool:
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):
def __init__(self, pkg_id: str, version: str, pkg_type: str, name: str):
"""
:param pkg_id: an unique package identifier
:param version: the new version
:param pkg_type: the package type
"""
self.id = pkg_id
self.name = name
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)
return '{} (id={}, name={}, version={}, type={})'.format(self.__class__.__name__, self.id, self.name, self.version, self.type)
class PackageHistory:

View File

@@ -75,7 +75,8 @@ class SelectViewType(Enum):
class SingleSelectComponent(InputViewComponent):
def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None,
max_per_line: int = 1, tooltip: str = None, max_width: int = -1, id_: str = None):
max_per_line: int = 1, tooltip: str = None, max_width: int = -1, id_: str = None,
capitalize_label: bool = True):
super(SingleSelectComponent, self).__init__(id_=id_)
self.type = type_
self.label = label
@@ -84,6 +85,7 @@ class SingleSelectComponent(InputViewComponent):
self.max_per_line = max_per_line
self.tooltip = tooltip
self.max_width = max_width
self.capitalize_label = capitalize_label
def get_selected(self):
if self.value:
@@ -119,11 +121,12 @@ class MultipleSelectComponent(InputViewComponent):
class TextComponent(ViewComponent):
def __init__(self, html: str, max_width: int = -1, tooltip: str = None, id_: str = None):
def __init__(self, html: str, max_width: int = -1, tooltip: str = None, id_: str = None, size: int = None):
super(TextComponent, self).__init__(id_=id_)
self.value = html
self.max_width = max_width
self.tooltip = tooltip
self.size = size
class TwoStateButtonComponent(ViewComponent):
@@ -156,8 +159,10 @@ class TextInputComponent(ViewComponent):
def get_int_value(self) -> int:
if self.value is not None:
return int(self.value)
return None
val = self.value.strip() if isinstance(self.value, str) else self.value
if val:
return int(self.value)
class FormComponent(ViewComponent):

View File

@@ -1,7 +1,7 @@
import os
from pathlib import Path
CACHE_PATH = '{}/.cache/bauh'.format(Path.home())
CONFIG_PATH = '{}/.config/bauh'.format(Path.home())
DESKTOP_ENTRIES_DIR = '{}/.local/share/applications'.format(Path.home())
CACHE_PATH = '{}/.cache/bauh'.format(str(Path.home()))
CONFIG_PATH = '{}/.config/bauh'.format(str(Path.home()))
DESKTOP_ENTRIES_DIR = '{}/.local/share/applications'.format(str(Path.home()))
TEMP_DIR = '/tmp/bauh{}'.format('_root' if os.getuid() == 0 else '')

View File

@@ -67,7 +67,7 @@ class HttpClient:
res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session)
return yaml.safe_load(res.text) if res else None
def get_content_length(self, url: str, session: bool = True) -> str:
def get_content_length_in_bytes(self, url: str, session: bool = True) -> int:
params = {'url': url, 'allow_redirects': True, 'stream': True}
if session:
res = self.session.get(**params)
@@ -77,8 +77,17 @@ class HttpClient:
if res.status_code == 200:
size = res.headers.get('Content-Length')
if size is not None:
return system.get_human_size_str(size)
if size:
try:
return int(size)
except:
pass
def get_content_length(self, url: str, session: bool = True) -> str:
size = self.get_content_length_in_bytes(url, session)
if size:
return system.get_human_size_str(size)
def exists(self, url: str, session: bool = True) -> bool:
params = {'url': url, 'allow_redirects': True, 'verify': False, 'timeout': 5}