mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-07 00:04:15 +02:00
AUR bearhub: fix stale source cache collision (pkgrel=10)
Use tag-specific tarball name bearhub-0.10.7-bearhub.6.tar.gz so makepkg and AUR helpers do not reuse an outdated bearhub-0.10.7.tar.gz from cache. Ignore makepkg artifacts under packaging/aur/.
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Set, Optional
|
||||
|
||||
|
||||
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: Optional[int]) -> MemoryCache:
|
||||
"""
|
||||
:param expiration: expiration time for the cache keys in seconds. Use -1 to disable this feature.
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
@@ -1,75 +0,0 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional, Dict
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCacheFactory
|
||||
from bauh.api.abstract.disk import DiskCacheLoaderFactory
|
||||
from bauh.api.abstract.download import FileDownloader
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.commons.internet import InternetChecker
|
||||
from bauh.view.util.translation import I18n
|
||||
|
||||
|
||||
class ApplicationContext:
|
||||
|
||||
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,
|
||||
app_version: str, internet_checker: InternetChecker, root_user: bool, screen_width: int = -1,
|
||||
screen_height: int = -1, suggestions_mapping: Optional[Dict[str, str]] = None):
|
||||
"""
|
||||
: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 translation keys
|
||||
:param cache_factory:
|
||||
:param disk_loader_factory:
|
||||
:param logger: a logger instance
|
||||
:param file_downloader
|
||||
:param distro
|
||||
:param app_name
|
||||
:param app_version
|
||||
:param internet_checker
|
||||
:param screen_width
|
||||
:param screen_height
|
||||
:param suggestions_mapping
|
||||
"""
|
||||
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
|
||||
self.file_downloader = file_downloader
|
||||
self.arch_x86_64 = sys.maxsize > 2**32
|
||||
self.distro = distro
|
||||
self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game',
|
||||
'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility')
|
||||
self.app_name = app_name
|
||||
self.app_version = app_version
|
||||
self.root_user = root_user
|
||||
self.root_password = None
|
||||
self.internet_checker = internet_checker
|
||||
self.screen_width = screen_width
|
||||
self.screen_height = screen_height
|
||||
self._suggestions_mapping = suggestions_mapping
|
||||
|
||||
def is_system_x86_64(self):
|
||||
return self.arch_x86_64
|
||||
|
||||
def get_view_path(self):
|
||||
return self.app_root_dir + '/view'
|
||||
|
||||
def is_internet_available(self) -> bool:
|
||||
return self.internet_checker.is_available()
|
||||
|
||||
def get_suggestion_url(self, module: str, default: Optional[str] = None) -> Optional[str]:
|
||||
if self._suggestions_mapping:
|
||||
module_split = module.split(f'{self.app_name}.gems.')
|
||||
|
||||
if len(module_split) > 1:
|
||||
gem_name = module_split[1].split('.')[0]
|
||||
return self._suggestions_mapping.get(gem_name, default)
|
||||
|
||||
return default
|
||||
@@ -1,432 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import List, Set, Type, Tuple, Optional, Generator, TypeVar
|
||||
|
||||
import yaml
|
||||
|
||||
from bauh.api.abstract.context import ApplicationContext
|
||||
from bauh.api.abstract.disk import DiskCacheLoader
|
||||
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
|
||||
|
||||
P = TypeVar('P', bound=SoftwarePackage)
|
||||
|
||||
|
||||
class SearchResult:
|
||||
|
||||
def __init__(self, installed: Optional[List[P]], new: Optional[List[P]], 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
|
||||
|
||||
def update_total(self):
|
||||
total = 0
|
||||
|
||||
if self.installed:
|
||||
total += len(self.installed)
|
||||
|
||||
if self.new:
|
||||
total += len(self.new)
|
||||
|
||||
self.total = total
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> "SearchResult":
|
||||
return cls(installed=[], new=[], total=0)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, SearchResult):
|
||||
if self.installed == other.installed:
|
||||
return self.new == other.new
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.installed) + hash(self.new)
|
||||
|
||||
|
||||
class UpgradeRequirement:
|
||||
|
||||
def __init__(self, pkg: SoftwarePackage, reason: Optional[str] = None, required_size: Optional[float] = None, extra_size: Optional[float] = None, sorting_priority: int = 0):
|
||||
"""
|
||||
|
||||
: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
|
||||
:param sorting_priority: an int representing the sorting priority (higher numbers = higher priority)
|
||||
"""
|
||||
self.pkg = pkg
|
||||
self.reason = reason
|
||||
self.required_size = required_size
|
||||
self.extra_size = extra_size
|
||||
self.sorting_priority = sorting_priority
|
||||
|
||||
@staticmethod
|
||||
def sort_by_priority(req: "UpgradeRequirement") -> Tuple[int, str]:
|
||||
return -req.sorting_priority, req.pkg.name
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, UpgradeRequirement):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return sum((hash(k) + hash(v) for k, v in self.__dict__.items()))
|
||||
|
||||
|
||||
class UpgradeRequirements:
|
||||
|
||||
def __init__(self, to_install: Optional[List[UpgradeRequirement]], to_remove: Optional[List[UpgradeRequirement]],
|
||||
to_upgrade: Optional[List[UpgradeRequirement]], cannot_upgrade: Optional[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
|
||||
self.context = {} # caches relevant data to actually perform the upgrade
|
||||
|
||||
|
||||
class TransactionResult:
|
||||
"""
|
||||
The result of a given operation
|
||||
"""
|
||||
|
||||
def __init__(self, success: bool, installed: Optional[List[SoftwarePackage]], removed: Optional[List[SoftwarePackage]]):
|
||||
self.success = success
|
||||
self.installed = installed
|
||||
self.removed = removed
|
||||
|
||||
@staticmethod
|
||||
def fail() -> "TransactionResult":
|
||||
return TransactionResult(success=False, installed=None, removed=None)
|
||||
|
||||
|
||||
class SoftwareAction(Enum):
|
||||
PREPARE = 0
|
||||
SEARCH = 1
|
||||
INSTALL = 2
|
||||
UNINSTALL = 3
|
||||
UPGRADE = 4
|
||||
DOWNGRADE = 5
|
||||
|
||||
|
||||
class SettingsController(ABC):
|
||||
|
||||
def save_settings(self, component: ViewComponent) -> Tuple[bool, Optional[List[str]]]:
|
||||
"""
|
||||
:return: a tuple with a bool informing if the settings were saved and a list of error messages
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class SettingsView:
|
||||
|
||||
def __init__(self, controller: SettingsController, component: ViewComponent, label: Optional[str] = None,
|
||||
icon_path: Optional[str] = None):
|
||||
|
||||
self.controller = controller
|
||||
self.component = component
|
||||
self.label = label
|
||||
self.icon_path = icon_path
|
||||
|
||||
def save(self) -> Tuple[bool, Optional[List[str]]]:
|
||||
return self.controller.save_settings(self.component)
|
||||
|
||||
|
||||
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: Optional[DiskCacheLoader], limit: int, is_url: bool) -> 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
|
||||
:param is_url: if "words" is a URL
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int, only_apps: bool, pkg_types: Optional[Set[Type[SoftwarePackage]]], internet_available: bool) -> 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
|
||||
:param internet_available: if there is internet connection
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def downgrade(self, pkg: SoftwarePackage, root_password: Optional[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())
|
||||
|
||||
def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: Optional[str], watcher: ProcessWatcher) -> UpgradeRequirements:
|
||||
"""
|
||||
return additional required software that needs to be installed / removed / updated before updating a list of packages
|
||||
:param pkgs:
|
||||
:param watcher
|
||||
:return:
|
||||
"""
|
||||
return UpgradeRequirements(None, None, [UpgradeRequirement(p) for p in pkgs], None)
|
||||
|
||||
@abstractmethod
|
||||
def upgrade(self, requirements: UpgradeRequirements, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
|
||||
"""
|
||||
:param requirements:
|
||||
:param root_password: the root user password (if required)
|
||||
:param watcher:
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def uninstall(self, pkg: SoftwarePackage, root_password: Optional[str], watcher: ProcessWatcher, disk_loader: Optional[DiskCacheLoader]) -> TransactionResult:
|
||||
"""
|
||||
:param pkg:
|
||||
:param root_password: the root user password (if required)
|
||||
:param watcher:
|
||||
:param disk_loader:
|
||||
:return:
|
||||
"""
|
||||
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: Optional[str], disk_loader: Optional[DiskCacheLoader], watcher: ProcessWatcher) -> TransactionResult:
|
||||
"""
|
||||
:param pkg:
|
||||
:param root_password: the root user password (if required)
|
||||
:param disk_loader
|
||||
:param watcher:
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_enabled(self) -> bool:
|
||||
"""
|
||||
:return: if the instance is enabled
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_enabled(self, enabled: bool):
|
||||
"""
|
||||
:param enabled:
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def can_work(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
:return: if the instance can work based on what is installed in the user's machine. If not, an optional string as a reason.
|
||||
"""
|
||||
|
||||
def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: Optional[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():
|
||||
self.serialize_to_disk(pkg, icon_bytes, only_icon)
|
||||
|
||||
def serialize_to_disk(self, pkg: SoftwarePackage, icon_bytes: Optional[bytes], only_icon: bool):
|
||||
"""
|
||||
Sames as above, but does not check if disk cache is enabled or supported by the package instance
|
||||
:param pkg:
|
||||
:param icon_bytes:
|
||||
:param only_icon:
|
||||
:return:
|
||||
"""
|
||||
if not only_icon:
|
||||
Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
|
||||
data = pkg.get_data_to_cache()
|
||||
|
||||
if data:
|
||||
disk_path = pkg.get_disk_data_path()
|
||||
ext = disk_path.split('.')[-1]
|
||||
|
||||
if ext == 'json':
|
||||
with open(disk_path, 'w+') as f:
|
||||
f.write(json.dumps(data))
|
||||
elif ext in ('yml', 'yaml'):
|
||||
with open(disk_path, 'w+') as f:
|
||||
f.write(yaml.dump(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: SoftwareAction, pkg: Optional[SoftwarePackage]) -> bool:
|
||||
"""
|
||||
if a given action requires root privileges to be executed. 'install', 'uninstall', 'downgrade', 'search', 'prepare'
|
||||
:param action:
|
||||
:param pkg:
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def prepare(self, task_manager: Optional[TaskManager], root_password: Optional[str], internet_available: Optional[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
|
||||
:param internet_available: if there is internet connection available
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
|
||||
"""
|
||||
:param internet_available
|
||||
:return: available package updates
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_warnings(self, internet_available: bool) -> Optional[List[str]]:
|
||||
"""
|
||||
:param internet_available
|
||||
:return: a list of warnings to be shown to the user
|
||||
"""
|
||||
pass
|
||||
|
||||
def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[PackageSuggestion]]:
|
||||
"""
|
||||
:param limit: max suggestions to be returned. If limit < 0, it should not be considered
|
||||
:param filter_installed: if the installed suggestions should not be retrieved
|
||||
:return: a list of package suggestions
|
||||
"""
|
||||
pass
|
||||
|
||||
def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: Optional[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
|
||||
|
||||
@abstractmethod
|
||||
def is_default_enabled(self) -> bool:
|
||||
"""
|
||||
:return: if the instance is enabled by default when there is no user settings defining which gems are enabled.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def launch(self, pkg: SoftwarePackage):
|
||||
pass
|
||||
|
||||
def get_screenshots(self, pkg: SoftwarePackage) -> Generator[str, None, None]:
|
||||
"""
|
||||
:return: yields screenshot urls for the given package
|
||||
"""
|
||||
pass
|
||||
|
||||
def clear_data(self, logs: bool = True):
|
||||
"""
|
||||
Removes all data created by the SoftwareManager instance
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_settings(self) -> Optional[Generator[SettingsView, None, None]]:
|
||||
"""
|
||||
:return: panel abstractions with optional icon paths associated with
|
||||
"""
|
||||
pass
|
||||
|
||||
def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]:
|
||||
"""
|
||||
:return: generates available custom actions
|
||||
"""
|
||||
yield from ()
|
||||
|
||||
def fill_sizes(self, pkgs: List[SoftwarePackage]):
|
||||
pass
|
||||
|
||||
def ignore_update(self, pkg: SoftwarePackage):
|
||||
pass
|
||||
|
||||
def revert_ignored_update(self, pkg: SoftwarePackage):
|
||||
pass
|
||||
@@ -1,53 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Type, Optional, Any, Dict
|
||||
|
||||
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, sync: bool = False):
|
||||
"""
|
||||
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:
|
||||
:param sync: if the package data must be filled synchronously
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def read(self, pkg: SoftwarePackage) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
returns the cached data from the given package
|
||||
"""
|
||||
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
|
||||
|
||||
@abstractmethod
|
||||
def new(self) -> DiskCacheLoader:
|
||||
pass
|
||||
@@ -1,47 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
|
||||
from bauh.api.abstract.handler import ProcessWatcher
|
||||
|
||||
|
||||
class FileDownloader(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def download(self, file_url: str, watcher: Optional[ProcessWatcher], output_path: str, cwd: str, root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, max_threads: int = None, known_size: int = None) -> bool:
|
||||
"""
|
||||
:param file_url:
|
||||
:param watcher:
|
||||
:param output_path: the downloaded file output path. Leave None for the current directory and the same file name
|
||||
:param cwd: current working directory. Leave None if does not matter.
|
||||
:param root_password: (if the output directory is protected)
|
||||
:param substatus_prefix: custom substatus prefix ('prefix downloading xpto')
|
||||
:param display_file_size: if the file size should be displayed on the substatus
|
||||
:param max_threads: maximum number of threads (only available for multi-threaded download)
|
||||
:param known_size: known file size
|
||||
:return: success / failure
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_multithreaded(self) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def can_work(self) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_supported_multithreaded_clients(self) -> Tuple[str, ...]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_multithreaded_client_available(self, name: str) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_available_multithreaded_clients(self) -> Tuple[str, ...]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_supported_clients(self) -> Tuple[str, ...]:
|
||||
pass
|
||||
@@ -1,128 +0,0 @@
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from bauh.api.abstract.view import MessageType, ViewComponent
|
||||
|
||||
|
||||
class ProcessWatcher:
|
||||
"""
|
||||
Represents an view component watching background processes. It's a bridge for ApplicationManager instances notify 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: Optional[str], components: List[ViewComponent] = None,
|
||||
confirmation_label: str = None, deny_label: str = None, deny_button: bool = True,
|
||||
window_cancel: bool = False, confirmation_button: bool = True,
|
||||
min_width: Optional[int] = None,
|
||||
min_height: Optional[int] = None,
|
||||
max_width: Optional[int] = 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')
|
||||
:param deny_button: if the deny button should be displayed
|
||||
:param window_cancel: if the window cancel button should be visible
|
||||
:param confirmation_button: if the confirmation button should be displayed
|
||||
:param min_width: minimum width for the confirmation dialog
|
||||
:param min_height: minimum height for the confirmation dialog
|
||||
:param max_width: maximum width for the confirmation dialog
|
||||
: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.
|
||||
: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.
|
||||
"""
|
||||
|
||||
def request_root_password(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
asks the root password for the user
|
||||
:return: a tuple informing if the password is valid and its text
|
||||
"""
|
||||
|
||||
|
||||
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: Optional[str]):
|
||||
"""
|
||||
:param task_id:
|
||||
:param progress: a float between 0 and 100.
|
||||
:param substatus: optional substatus string representing the current state
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def update_output(self, task_id: str, output: str):
|
||||
"""
|
||||
updates the task output
|
||||
:param task_id:
|
||||
:param output:
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def finish_task(self, task_id: str):
|
||||
"""
|
||||
marks a task as finished
|
||||
:param task_id:
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
@@ -1,320 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Iterable
|
||||
|
||||
from bauh.api.paths import CACHE_DIR
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from bauh.api.abstract.controller import SoftwareManager
|
||||
|
||||
|
||||
class CustomSoftwareAction:
|
||||
|
||||
def __init__(self, i18n_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str,
|
||||
requires_root: bool, manager: SoftwareManager = None,
|
||||
backup: bool = False, refresh: bool = True,
|
||||
i18n_confirm_key: str = None,
|
||||
requires_internet: bool = False,
|
||||
requires_confirmation: bool = True,
|
||||
i18n_description_key: Optional[str] = None):
|
||||
"""
|
||||
:param i18n_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: Optional[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:
|
||||
:param refresh: if all displayed apps on the view should be refreshed if the action succeeds
|
||||
:param i18n_confirm_key: action confirmation message
|
||||
:param requires_internet: if the action requires internet connection to be executed
|
||||
:param requires_confirmation: if a confirmation popup should be displayed to the user before calling the action
|
||||
:param i18n_description_key: the i18n key for the action description
|
||||
"""
|
||||
self.i18n_label_key = i18n_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
|
||||
self.refresh = refresh
|
||||
self.i18n_confirm_key = i18n_confirm_key
|
||||
self.requires_internet = requires_internet
|
||||
self.requires_confirmation = requires_confirmation
|
||||
self.i18n_description_key = i18n_description_key
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return sum(hash(val) for val in self.__dict__.values())
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, CustomSoftwareAction):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__} (label={self.i18n_label_key}, method={self.manager_method})"
|
||||
|
||||
|
||||
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, license: 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
|
||||
self.license = license
|
||||
self.gem_name = self.__module__.split('.')[2]
|
||||
|
||||
@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
|
||||
|
||||
def is_update_ignored(self) -> bool:
|
||||
return False
|
||||
|
||||
def supports_ignored_updates(self) -> bool:
|
||||
return False
|
||||
|
||||
@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 f'{CACHE_DIR}/{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):
|
||||
path = self.get_disk_cache_path()
|
||||
if path:
|
||||
return '{}/icon.png'.format(path)
|
||||
|
||||
def get_disk_data_path(self):
|
||||
path = self.get_disk_cache_path()
|
||||
if path:
|
||||
return '{}/data.json'.format(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) -> bool:
|
||||
"""
|
||||
:return: whether the app can be run via the GUI
|
||||
"""
|
||||
|
||||
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_actions(self) -> Optional[Iterable[CustomSoftwareAction]]:
|
||||
"""
|
||||
:return: custom supported actions
|
||||
"""
|
||||
pass
|
||||
|
||||
def has_screenshots(self) -> bool:
|
||||
"""
|
||||
:return: if there are screenshots to be displayed
|
||||
"""
|
||||
return not self.installed
|
||||
|
||||
def get_name_tooltip(self) -> str:
|
||||
"""
|
||||
:return: the application name that should be displayed on the UI tooltips.
|
||||
"""
|
||||
return self.name
|
||||
|
||||
def get_display_name(self) -> str:
|
||||
"""
|
||||
:return: name displayed on the table
|
||||
"""
|
||||
return self.name
|
||||
|
||||
def get_update_tip(self) -> Optional[str]:
|
||||
"""
|
||||
custom 'version' update tooltip
|
||||
"""
|
||||
return
|
||||
|
||||
@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, 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):
|
||||
attrs = ', '.join(f'{p}={v}' for p, v in sorted(self.__dict__.items()))
|
||||
return f'{self.__class__.__name__} ({attrs})'
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, PackageUpdate):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return sum(hash(v) for v in self.__dict__.values())
|
||||
|
||||
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def empyt(cls, pkg: SoftwarePackage):
|
||||
return cls(pkg=pkg, history=[], pkg_status_idx=-1)
|
||||
|
||||
|
||||
class SuggestionPriority(Enum):
|
||||
LOW = 0
|
||||
MEDIUM = 1
|
||||
HIGH = 2
|
||||
TOP = 3
|
||||
|
||||
def __gt__(self, other):
|
||||
if isinstance(other, SuggestionPriority):
|
||||
return self.value > other.value
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, SuggestionPriority):
|
||||
return self.value < other.value
|
||||
|
||||
|
||||
class PackageSuggestion:
|
||||
|
||||
def __init__(self, package: SoftwarePackage, priority: SuggestionPriority):
|
||||
self.package = package
|
||||
self.priority = priority
|
||||
@@ -1,340 +0,0 @@
|
||||
from abc import ABC
|
||||
from enum import Enum
|
||||
from typing import List, Set, Optional, Dict, TypeVar, Type, Union, Tuple
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
INFO = 0
|
||||
WARNING = 1
|
||||
ERROR = 2
|
||||
|
||||
|
||||
class ViewComponentAlignment(Enum):
|
||||
CENTER = 0
|
||||
LEFT = 1
|
||||
RIGHT = 2
|
||||
BOTTOM = 3
|
||||
TOP = 4
|
||||
HORIZONTAL_CENTER = 5
|
||||
VERTICAL_CENTER = 6
|
||||
|
||||
|
||||
class ViewObserver:
|
||||
|
||||
def on_change(self, change):
|
||||
pass
|
||||
|
||||
|
||||
class ViewComponent(ABC):
|
||||
"""
|
||||
Represents a GUI component
|
||||
"""
|
||||
def __init__(self, id_: Optional[str], alignment: Optional[ViewComponentAlignment] = None,
|
||||
observers: Optional[List[ViewObserver]] = None):
|
||||
self.id = id_
|
||||
self.alignment = alignment
|
||||
self.observers = observers if observers else []
|
||||
|
||||
def add_observer(self, obs):
|
||||
self.observers.append(obs)
|
||||
|
||||
|
||||
V = TypeVar('V', bound=ViewComponent)
|
||||
|
||||
|
||||
class ViewContainer(ViewComponent, ABC):
|
||||
"""
|
||||
Represents a GUI component composed by other components
|
||||
"""
|
||||
|
||||
def __init__(self, components: Union[List[V], Tuple[V]],
|
||||
id_: Optional[str] = None,
|
||||
observers: Optional[List[ViewObserver]] = None):
|
||||
super(ViewContainer, self).__init__(id_=id_, observers=observers)
|
||||
self.components = components
|
||||
self.component_map = {c.id: c for c in components if c.id is not None} if components else None
|
||||
|
||||
def get_component(self, id_: str, type_: Type[V] = ViewComponent) -> Optional[V]:
|
||||
if self.component_map:
|
||||
instance = self.component_map.get(id_)
|
||||
|
||||
if instance:
|
||||
if isinstance(instance, type_):
|
||||
return instance
|
||||
|
||||
raise Exception(f"The {ViewComponent.__class__.__name__} (id={id_}) is not an "
|
||||
f"instance of '{type_.__name__}'")
|
||||
|
||||
def get_component_by_idx(self, idx: int, type_: Type[V]) -> Optional[V]:
|
||||
if self.components and idx < len(self.components):
|
||||
c = self.components[idx]
|
||||
if isinstance(c, type_):
|
||||
return c
|
||||
|
||||
raise Exception(f"The {ViewComponent.__class__.__name__} at index {idx} is not an "
|
||||
f"instance of '{type_.__name__}'")
|
||||
|
||||
|
||||
class SpacerComponent(ViewComponent):
|
||||
|
||||
def __init__(self):
|
||||
super(SpacerComponent, self).__init__(id_=None)
|
||||
|
||||
|
||||
class InputViewComponent(ViewComponent):
|
||||
"""
|
||||
Represents a component which needs a user interaction to provide its value
|
||||
"""
|
||||
|
||||
|
||||
class InputOption:
|
||||
"""
|
||||
Represents a select component option.
|
||||
"""
|
||||
|
||||
def __init__(self, label: str, value: object, tooltip: Optional[str] = None,
|
||||
icon_path: Optional[str] = None, read_only: bool = False, id_: Optional[str] = None,
|
||||
invalid: bool = False, extra_properties: Optional[Dict[str, 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
|
||||
:param invalid: if this option is considered invalid
|
||||
:param extra_properties: extra properties
|
||||
"""
|
||||
if not label:
|
||||
raise Exception("'label' must be a not blank string")
|
||||
|
||||
self.id = id_
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.tooltip = tooltip
|
||||
self.icon_path = icon_path
|
||||
self.read_only = read_only
|
||||
self.invalid = invalid
|
||||
self.extra_properties = extra_properties
|
||||
|
||||
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, tooltip: str = None,
|
||||
max_width: Optional[int] = -1, id_: str = None, capitalize_label: bool = True,
|
||||
alignment: Optional[ViewComponentAlignment] = None):
|
||||
super(SingleSelectComponent, self).__init__(id_=id_, alignment=alignment)
|
||||
self.type = type_
|
||||
self.label = label
|
||||
self.options = options
|
||||
self.value = default_option
|
||||
self.init_value = default_option
|
||||
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:
|
||||
return self.value.value
|
||||
|
||||
def changed(self) -> bool:
|
||||
return self.init_value != self.value
|
||||
|
||||
|
||||
class MultipleSelectComponent(InputViewComponent):
|
||||
|
||||
def __init__(self, label: Optional[str], options: List[InputOption], default_options: Set[InputOption] = None,
|
||||
max_per_line: int = 1, tooltip: str = None, spaces: bool = True, max_width: int = -1,
|
||||
max_height: int = -1, id_: str = None, min_width: Optional[int] = None,
|
||||
opt_max_width: Optional[int] = None):
|
||||
super(MultipleSelectComponent, self).__init__(id_=id_)
|
||||
|
||||
if not options:
|
||||
raise Exception("'options' cannot be None or empty")
|
||||
|
||||
self.options = options
|
||||
self.spaces = spaces
|
||||
self.label = label
|
||||
self.tooltip = tooltip
|
||||
self.values = default_options if default_options else set()
|
||||
self.max_per_line = max_per_line
|
||||
self.min_width = min_width
|
||||
self.max_width = max_width
|
||||
self.max_height = max_height
|
||||
self.opt_max_width = opt_max_width
|
||||
|
||||
def get_selected_values(self) -> list:
|
||||
selected = []
|
||||
if self.values:
|
||||
selected.extend([op.value for op in self.values])
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
class TextComponent(ViewComponent):
|
||||
|
||||
def __init__(self, html: str, min_width: Optional[int] = None, max_width: int = -1,
|
||||
tooltip: str = None, id_: str = None, size: int = None):
|
||||
super(TextComponent, self).__init__(id_=id_)
|
||||
self.value = html
|
||||
self.min_width = min_width
|
||||
self.max_width = max_width
|
||||
self.tooltip = tooltip
|
||||
self.size = size
|
||||
|
||||
|
||||
class TwoStateButtonComponent(ViewComponent):
|
||||
|
||||
def __init__(self, label: str, tooltip: str = None, state: bool = False, id_: str = None):
|
||||
super(TwoStateButtonComponent, self).__init__(id_=id_)
|
||||
self.label = label
|
||||
self.tooltip = tooltip
|
||||
self.state = state
|
||||
|
||||
|
||||
class TextInputType(Enum):
|
||||
SINGLE_LINE = 0
|
||||
MULTIPLE_LINES = 1
|
||||
|
||||
|
||||
class TextInputComponent(ViewComponent):
|
||||
|
||||
def __init__(self, label: str, value: str = '', placeholder: Optional[str] = None, tooltip: Optional[str] = None,
|
||||
read_only: bool = False, id_: Optional[str] = None, only_int: bool = False, max_width: int = -1,
|
||||
type_: TextInputType = TextInputType.SINGLE_LINE, capitalize_label: bool = True, min_width: int = -1,
|
||||
min_height: int = -1):
|
||||
super(TextInputComponent, self).__init__(id_=id_)
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.tooltip = tooltip
|
||||
self.placeholder = placeholder
|
||||
self.read_only = read_only
|
||||
self.only_int = only_int
|
||||
self.max_width = max_width
|
||||
self.type = type_
|
||||
self.capitalize_label = capitalize_label
|
||||
self.min_width = min_width
|
||||
self.min_height = min_height
|
||||
|
||||
def get_value(self) -> str:
|
||||
if self.value is not None:
|
||||
return self.value
|
||||
else:
|
||||
return ''
|
||||
|
||||
def set_value(self, val: Optional[str], caller: object = None):
|
||||
if val != self.value:
|
||||
self.value = val
|
||||
|
||||
if self.observers:
|
||||
for o in self.observers:
|
||||
if caller != o:
|
||||
o.on_change(val)
|
||||
|
||||
def get_int_value(self) -> Optional[int]:
|
||||
if self.value is not None:
|
||||
val = self.value.strip() if isinstance(self.value, str) else self.value
|
||||
|
||||
if val is not None:
|
||||
return int(self.value)
|
||||
|
||||
def get_label(self) -> str:
|
||||
if not self.label:
|
||||
return ''
|
||||
else:
|
||||
return self.label.capitalize() if self.capitalize_label else self.label
|
||||
|
||||
|
||||
class FormComponent(ViewContainer):
|
||||
|
||||
def __init__(self, components: List[ViewComponent], label: str = None, spaces: bool = True, id_: str = None,
|
||||
min_width: Optional[int] = None):
|
||||
super(FormComponent, self).__init__(id_=id_, components=components)
|
||||
self.label = label
|
||||
self.spaces = spaces
|
||||
self.min_width = min_width
|
||||
|
||||
|
||||
class FileChooserComponent(ViewComponent):
|
||||
|
||||
def __init__(self, allowed_extensions: Optional[Set[str]] = None, label: Optional[str] = None, tooltip: Optional[str] = None,
|
||||
file_path: Optional[str] = None, max_width: int = -1, id_: Optional[str] = None,
|
||||
search_path: Optional[str] = None, capitalize_label: bool = True, directory: bool = False):
|
||||
super(FileChooserComponent, self).__init__(id_=id_)
|
||||
self.label = label
|
||||
self.allowed_extensions = allowed_extensions
|
||||
self.file_path = file_path
|
||||
self.tooltip = tooltip
|
||||
self.max_width = max_width
|
||||
self.search_path = search_path
|
||||
self.capitalize_label = capitalize_label
|
||||
self.directory = directory
|
||||
|
||||
def set_file_path(self, fpath: Optional[str]):
|
||||
self.file_path = fpath
|
||||
|
||||
if self.observers:
|
||||
for o in self.observers:
|
||||
o.on_change(self.file_path)
|
||||
|
||||
def get_label(self) -> str:
|
||||
if not self.label:
|
||||
return ''
|
||||
else:
|
||||
return self.label.capitalize() if self.capitalize_label else self.label
|
||||
|
||||
|
||||
class TabComponent(ViewComponent):
|
||||
|
||||
def __init__(self, label: str, content: ViewComponent, icon_path: str = None, id_: str = None):
|
||||
super(TabComponent, self).__init__(id_=id_)
|
||||
self.label = label
|
||||
self._content = content
|
||||
self.icon_path = icon_path
|
||||
|
||||
def get_content(self, type_: Type[V] = ViewComponent) -> V:
|
||||
if isinstance(self._content, type_):
|
||||
return self._content
|
||||
|
||||
raise Exception(f"'content' is not an instance of '{type_.__name__}'")
|
||||
|
||||
|
||||
class TabGroupComponent(ViewContainer):
|
||||
|
||||
def __init__(self, tabs: List[TabComponent], id_: str = None):
|
||||
super(TabGroupComponent, self).__init__(id_=id_, components=tabs)
|
||||
|
||||
def get_tab(self, id_: str) -> Optional[TabComponent]:
|
||||
return self.get_component(id_, TabComponent)
|
||||
|
||||
@property
|
||||
def tabs(self) -> List[TabComponent]:
|
||||
return self.components
|
||||
|
||||
|
||||
class RangeInputComponent(InputViewComponent):
|
||||
|
||||
def __init__(self, id_: str, label: str, tooltip: str, min_value: int, max_value: int,
|
||||
step_value: int, value: Optional[int] = None, max_width: int = None):
|
||||
super(RangeInputComponent, self).__init__(id_=id_)
|
||||
self.label = label
|
||||
self.tooltip = tooltip
|
||||
self.min = min_value
|
||||
self.max = max_value
|
||||
self.step = step_value
|
||||
self.value = value
|
||||
self.max_width = max_width
|
||||
|
||||
|
||||
class PanelComponent(ViewContainer):
|
||||
|
||||
def __init__(self, components: List[ViewComponent], id_: str = None):
|
||||
super(PanelComponent, self).__init__(id_=id_, components=components)
|
||||
@@ -1,5 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class NoInternetException(Exception):
|
||||
pass
|
||||
121
bauh/api/http.py
121
bauh/api/http.py
@@ -1,121 +0,0 @@
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
from bauh.commons import system
|
||||
from bauh.commons.view_utils import get_human_size_str
|
||||
|
||||
|
||||
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, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False,
|
||||
session: bool = True, stream: bool = False) -> Optional[requests.Response]:
|
||||
cur_attempts = 1
|
||||
|
||||
while cur_attempts <= self.max_attempts:
|
||||
cur_attempts += 1
|
||||
|
||||
try:
|
||||
args = {'timeout': self.timeout, 'allow_redirects': allow_redirects, 'stream': stream}
|
||||
|
||||
if params:
|
||||
args['params'] = params
|
||||
|
||||
if headers:
|
||||
args['headers'] = headers
|
||||
|
||||
if ignore_ssl:
|
||||
args['verify'] = False
|
||||
|
||||
if session:
|
||||
res = self.session.get(url, **args)
|
||||
else:
|
||||
res = requests.get(url, **args)
|
||||
|
||||
if 200 <= res.status_code < 300:
|
||||
return res
|
||||
|
||||
if single_call:
|
||||
return res
|
||||
|
||||
if self.sleep > 0:
|
||||
time.sleep(self.sleep)
|
||||
except Exception as e:
|
||||
if isinstance(e, requests.exceptions.ConnectionError):
|
||||
self.logger.error('Internet seems to be off')
|
||||
raise e
|
||||
elif isinstance(e, requests.exceptions.TooManyRedirects):
|
||||
self.logger.warning(f"Too many redirects for GET -> {url}")
|
||||
raise e
|
||||
elif e.__class__ in (requests.exceptions.MissingSchema, requests.exceptions.InvalidSchema):
|
||||
self.logger.warning(f"The URL '{url}' has an invalid schema")
|
||||
raise e
|
||||
|
||||
self.logger.error(f"Could not retrieve data from '{url}'")
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
self.logger.warning(f"Could not retrieve data from '{url}'")
|
||||
|
||||
def get_json(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True):
|
||||
res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session)
|
||||
return res.json() if res else None
|
||||
|
||||
def get_yaml(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True):
|
||||
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_in_bytes(self, url: str, session: bool = True) -> Optional[int]:
|
||||
if not url:
|
||||
return
|
||||
|
||||
params = {'url': url, 'allow_redirects': True, 'stream': True}
|
||||
|
||||
try:
|
||||
if session:
|
||||
res = self.session.get(**params)
|
||||
else:
|
||||
res = requests.get(**params)
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.logger.info(f"Internet seems to be off. Could not reach '{url}'")
|
||||
return
|
||||
|
||||
if res.status_code == 200:
|
||||
size = res.headers.get('Content-Length')
|
||||
|
||||
if size:
|
||||
try:
|
||||
return int(size)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_content_length(self, url: str, session: bool = True) -> Optional[str]:
|
||||
size = self.get_content_length_in_bytes(url, session)
|
||||
|
||||
if size:
|
||||
return get_human_size_str(size)
|
||||
|
||||
def exists(self, url: str, session: bool = True, timeout: int = 5) -> bool:
|
||||
params = {'url': url, 'allow_redirects': True, 'verify': False, 'timeout': timeout}
|
||||
|
||||
try:
|
||||
if session:
|
||||
res = self.session.head(**params)
|
||||
else:
|
||||
res = self.session.get(**params)
|
||||
except requests.exceptions.TooManyRedirects:
|
||||
self.logger.warning(f"{url} seems to exist, but too many redirects have happened")
|
||||
return True
|
||||
|
||||
return res.status_code in (200, 403)
|
||||
@@ -1,70 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
from getpass import getuser
|
||||
from pathlib import Path
|
||||
|
||||
from bauh import __app_name__
|
||||
from bauh.api import user
|
||||
|
||||
|
||||
def get_temp_dir(username: str) -> str:
|
||||
return f'/tmp/{__app_name__}@{username}'
|
||||
|
||||
|
||||
CACHE_DIR = f'/var/cache/{__app_name__}' if user.is_root() else f'{Path.home()}/.cache/{__app_name__}'
|
||||
CONFIG_DIR = f'/etc/{__app_name__}' if user.is_root() else f'{Path.home()}/.config/{__app_name__}'
|
||||
USER_THEMES_DIR = f'/usr/share/{__app_name__}/themes' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}/themes'
|
||||
DESKTOP_ENTRIES_DIR = '/usr/share/applications' if user.is_root() else f'{Path.home()}/.local/share/applications'
|
||||
TEMP_DIR = get_temp_dir(getuser())
|
||||
LOGS_DIR = f'{TEMP_DIR}/logs'
|
||||
AUTOSTART_DIR = f'/etc/xdg/autostart' if user.is_root() else f'{Path.home()}/.config/autostart'
|
||||
BINARIES_DIR = f'/usr/local/bin' if user.is_root() else f'{Path.home()}/.local/bin'
|
||||
SHARED_FILES_DIR = f'/usr/local/share/{__app_name__}' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}'
|
||||
|
||||
|
||||
def _migrate_dir_if_possible(old_path: str, new_path: str):
|
||||
if old_path == new_path:
|
||||
return
|
||||
|
||||
if not os.path.exists(old_path):
|
||||
return
|
||||
|
||||
if os.path.exists(new_path):
|
||||
return
|
||||
|
||||
try:
|
||||
parent = os.path.dirname(new_path)
|
||||
if parent:
|
||||
Path(parent).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
shutil.move(old_path, new_path)
|
||||
except Exception:
|
||||
# The migration is best-effort. If it fails, the app can still start with empty defaults.
|
||||
pass
|
||||
|
||||
|
||||
def migrate_legacy_paths():
|
||||
old_app_name = 'bauh'
|
||||
|
||||
if old_app_name == __app_name__:
|
||||
return
|
||||
|
||||
if user.is_root():
|
||||
old_cache = f'/var/cache/{old_app_name}'
|
||||
old_config = f'/etc/{old_app_name}'
|
||||
old_shared = f'/usr/local/share/{old_app_name}'
|
||||
old_temp = f'/tmp/{old_app_name}@{getuser()}'
|
||||
else:
|
||||
home = Path.home()
|
||||
old_cache = f'{home}/.cache/{old_app_name}'
|
||||
old_config = f'{home}/.config/{old_app_name}'
|
||||
old_shared = f'{home}/.local/share/{old_app_name}'
|
||||
old_temp = f'/tmp/{old_app_name}@{getuser()}'
|
||||
|
||||
_migrate_dir_if_possible(old_cache, CACHE_DIR)
|
||||
_migrate_dir_if_possible(old_config, CONFIG_DIR)
|
||||
_migrate_dir_if_possible(old_shared, SHARED_FILES_DIR)
|
||||
_migrate_dir_if_possible(old_temp, TEMP_DIR)
|
||||
|
||||
|
||||
migrate_legacy_paths()
|
||||
@@ -1,6 +0,0 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def is_root(user_id: Optional[int] = None):
|
||||
return user_id == 0 if user_id is not None else os.getuid() == 0
|
||||
Reference in New Issue
Block a user