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:
Sebastian Palencsar
2026-06-27 10:02:45 +02:00
parent c56b8a2473
commit da377aecdc
198 changed files with 4543 additions and 4644 deletions

View File

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -1,5 +0,0 @@
from typing import Optional
class NoInternetException(Exception):
pass

View File

@@ -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)

View File

@@ -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()

View File

@@ -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