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

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