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

@@ -1,57 +0,0 @@
import time
from logging import Logger
from threading import Thread
from typing import Optional
from bauh.api.abstract.handler import TaskManager
from bauh.commons.config import ConfigManager
from bauh.view.util.translation import I18n
class CreateConfigFile(Thread):
"""
Generic initialization task to create a configuration file
"""
def __init__(self, configman: ConfigManager, taskman: TaskManager, task_icon_path: str, i18n: I18n, logger: Logger, config_instance: Optional[dict] = None):
super(CreateConfigFile, self).__init__(daemon=True)
self.configman = configman
self.taskman = taskman
self.logger = logger
self.config = config_instance
self.task_icon_path = task_icon_path
self.task_id = configman.__class__.__name__
self.i18n = i18n
self.task_name = self.i18n['task.checking_config']
self.taskman.register_task(self.task_id, self.task_name, self.task_icon_path)
def _log(self, msg: str):
self.logger.info('{}: {}'.format(self.configman.__class__.__name__, msg))
def run(self):
ti = time.time()
self.taskman.update_progress(self.task_id, 1, None)
self._log("Reading cached configuration file")
default_config = self.configman.get_default_config()
cached_config = self.configman.read_config()
self.taskman.update_progress(self.task_id, 50, None)
if cached_config:
self._log("Merging configuration file")
self.configman.merge_config(default_config, cached_config)
else:
self._log("No cached configuration file found")
self.config = default_config
self.taskman.update_progress(self.task_id, 75, self.i18n['task.checking_config.saving'])
self._log("Writing configuration file")
self.configman.save_config(default_config)
self.taskman.update_progress(self.task_id, 100, None)
self.taskman.finish_task(self.task_id)
tf = time.time()
self._log("Finished. Took {0:.2f} seconds".format(tf - ti))

View File

@@ -1,174 +0,0 @@
import logging
import os
import time
import traceback
from datetime import datetime, timedelta, timezone
from pathlib import Path
from threading import Thread
from typing import Dict, List, Optional
import requests
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.http import HttpClient
from bauh.commons.internet import InternetChecker
from bauh.commons.util import map_timestamp_file
class CategoriesDownloader(Thread):
def __init__(self, id_: str, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager,
url_categories_file: str, categories_path: str, internet_checker: InternetChecker,
expiration: Optional[int] = None, internet_connection: Optional[bool] = True, before=None, after=None):
"""
:param id_:
:param http_client:
:param logger:
:param manager:
:param url_categories_file:
:param categories_path:
:param expiration: cached file expiration in hours
:param internet_checker
:param before:
:param after:
"""
super(CategoriesDownloader, self).__init__(daemon=True)
self.id_ = id_
self.http_client = http_client
self.logger = logger
self.manager = manager
self.url_categories_file = url_categories_file
self.categories_path = categories_path
self.before = before
self.after = after
self.expiration = expiration
self.internet_connection = internet_connection
self.internet_checker = internet_checker
def _msg(self, msg: str):
return '{} [{}]: {}'.format(self.__class__.__name__, self.id_, msg)
def _read_categories_from_disk(self) -> Dict[str, List[str]]:
if os.path.exists(self.categories_path):
self.logger.info(self._msg("Reading cached categories file {}".format(self.categories_path)))
with open(self.categories_path) as f:
categories = f.read()
return self._map_categories(categories)
else:
self.logger.warning("No cached categories file {} found".format(self.categories_path))
return {}
def _map_categories(self, categories: str) -> Dict[str, List[str]]:
categories_map = {}
for l in categories.split('\n'):
if l:
data = l.split('=')
categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c]
return categories_map
def _cache_categories_to_disk(self, categories_str: str, timestamp: float):
self.logger.info(self._msg('Caching downloaded categories to disk'))
try:
Path(os.path.dirname(self.categories_path)).mkdir(parents=True, exist_ok=True)
with open(self.categories_path, 'w+') as f:
f.write(categories_str)
self.logger.info(self._msg("Categories cached to file '{}'".format(self.categories_path)))
categories_ts_path = map_timestamp_file(self.categories_path)
with open(categories_ts_path, 'w+') as f:
f.write(str(timestamp))
self.logger.info(self._msg("Categories timestamp ({}) cached to file '{}'".format(timestamp, categories_ts_path)))
except Exception:
self.logger.error(self._msg("Could not cache categories to the disk as '{}'".format(self.categories_path)))
traceback.print_exc()
def download_categories(self) -> Dict[str, List[str]]:
self.logger.info(self._msg('Downloading category definitions from {}'.format(self.url_categories_file)))
try:
timestamp = datetime.now(timezone.utc).timestamp()
res = self.http_client.get(self.url_categories_file)
except requests.exceptions.ConnectionError:
self.logger.error(self._msg('[{}] Could not download categories. The internet connection seems to be off.'.format(self.id_)))
return {}
if not res:
self.logger.info(self._msg('Could not download {}'.format(self.url_categories_file)))
return {}
try:
categories = self._map_categories(res.text)
self.logger.info(self._msg('Loaded categories for {} applications'.format(len(categories))))
except Exception:
self.logger.error(self._msg("Could not parse categories definitions"))
traceback.print_exc()
return {}
if categories:
self._cache_categories_to_disk(categories_str=res.text, timestamp=timestamp)
return categories
def should_download(self) -> bool:
if self.internet_connection is False or (self.internet_connection is None and not self.internet_checker.is_available()):
self.logger.warning(self._msg("No internet connection. The categories file '{}' cannot be updated.".format(self.categories_path)))
return False
if self.expiration is None or self.expiration <= 0:
self.logger.warning(self._msg("No expiration set for the categories file '{}'. It should be downloaded".format(self.categories_path)))
return True
if not os.path.exists(self.categories_path):
self.logger.warning(self._msg("Categories file '{}' does not exist. It should be downloaded.".format(self.categories_path)))
return True
categories_ts_path = map_timestamp_file(self.categories_path)
if not os.path.exists(categories_ts_path):
self.logger.warning(self._msg("Categories timestamp file '{}' does not exist. The categories file should be re-downloaded.".format(categories_ts_path)))
return True
with open(categories_ts_path) as f:
timestamp_str = f.read()
try:
categories_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception:
self.logger.error(self._msg("An exception occurred when trying to parse the categories file timestamp from '{}'. The categories file should be re-downloaded.".format(categories_ts_path)))
traceback.print_exc()
return True
should_download = (categories_timestamp + timedelta(hours=self.expiration) <= datetime.now(timezone.utc))
if should_download:
self.logger.info(self._msg("Cached categories file '{}' has expired. A new one should be downloaded.".format(self.categories_path)))
return True
else:
self.logger.info(self._msg("Cached categories file '{}' is up to date. No need to re-download it.".format(self.categories_path)))
return False
def run(self):
ti = time.time()
if self.before:
self.before()
should_download = self.should_download()
if not should_download:
cached = self._read_categories_from_disk()
self.manager.categories = cached
else:
self.download_categories()
if self.after:
self.after()
tf = time.time()
self.logger.info(self._msg('Finished. Took {0:.2f} seconds'.format(tf - ti)))

View File

@@ -1,102 +0,0 @@
import os
import traceback
from abc import abstractmethod, ABC
from pathlib import Path
from threading import Thread
from typing import Optional
import yaml
from bauh.api.paths import CONFIG_DIR
from bauh.commons import util
def read_config(file_path: str, template: dict, update_file: bool = False, update_async: bool = False) -> dict:
if not os.path.exists(file_path):
Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True)
save_config(template, file_path)
else:
with open(file_path) as f:
local_config = yaml.safe_load(f.read())
if local_config:
util.deep_update(template, local_config)
if update_file:
if update_async:
Thread(target=save_config, args=(template, file_path), daemon=True).start()
else:
save_config(template, file_path)
return template
def save_config(config: dict, file_path: str):
with open(file_path, 'w+') as f:
f.write(yaml.dump(config))
class ConfigManager(ABC):
@abstractmethod
def read_config(self) -> Optional[dict]:
pass
@abstractmethod
def get_default_config(self) -> dict:
pass
@abstractmethod
def is_config_cached(self) -> bool:
pass
def get_config(self) -> dict:
default_config = self.get_default_config()
if default_config:
cached_config = self.read_config()
if cached_config:
self.merge_config(default_config, cached_config)
return default_config
@staticmethod
def merge_config(base_config: dict, current_config: dict):
util.deep_update(base_config, current_config)
@abstractmethod
def save_config(self, config_obj: dict):
pass
class YAMLConfigManager(ConfigManager, ABC):
def __init__(self, config_file_path: str):
self.file_path = config_file_path
def is_config_cached(self) -> bool:
return os.path.exists(self.file_path)
def read_config(self) -> Optional[dict]:
if self.is_config_cached():
with open(self.file_path) as f:
local_config = yaml.safe_load(f.read())
if local_config is not None:
return local_config
def save_config(self, config_obj: dict):
if config_obj:
config_dir = os.path.dirname(self.file_path)
try:
Path(config_dir).mkdir(parents=True, exist_ok=True)
except OSError:
traceback.print_exc()
return
try:
with open(self.file_path, 'w+') as f:
f.write(yaml.dump(config_obj))
except Exception:
traceback.print_exc()

View File

@@ -1,20 +0,0 @@
from typing import Optional, Any
class Value:
def __init__(self, value: Optional[Any] = None):
self.value = value
def __repr__(self):
return str(self.value)
def __str__(self):
return self.__repr__()
def __eq__(self, other):
if isinstance(other, Value):
return self.value == other.value
def __hash__(self):
return hash(self.value)

View File

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

View File

@@ -1,17 +0,0 @@
import socket
class InternetChecker:
def __init__(self, offline: bool):
self.offline = offline
def is_available(self) -> bool:
if self.offline:
return False
try:
socket.gethostbyname("w3.org")
return True
except Exception:
return False

View File

@@ -1,3 +0,0 @@
import re
RE_URL = re.compile(r"^https?://.+$")

View File

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

View File

@@ -1,9 +0,0 @@
class Singleton(type):
__instance = {}
def __call__(cls, *args, **kwargs):
if cls not in cls.__instance:
cls.__instance[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls.__instance[cls]

View File

@@ -1,35 +0,0 @@
from logging import Logger
from typing import Dict, Optional, Tuple
from bauh.api.abstract.model import SuggestionPriority
def parse(suggestions_str: str, logger: Optional[Logger] = None, type_: Optional[str] = None,
splitter: str = '=') \
-> Dict[str, SuggestionPriority]:
output = dict()
for line in suggestions_str.split('\n'):
clean_line = line.strip()
if clean_line:
line_split = clean_line.split(splitter, 1)
if len(line_split) == 2:
prio_str, name = line_split[0].strip(), line_split[1].strip()
if prio_str and name:
try:
prio = int(line_split[0])
except ValueError:
if logger:
logger.warning(f"Could not parse {type_ + ' ' if type_ else ''}suggestion: {line}")
continue
output[line_split[1]] = SuggestionPriority(prio)
return output
def sort_by_priority(names_prios: Dict[str, SuggestionPriority]) -> Tuple[str, ...]:
return tuple(pair[1] for pair in sorted(((names_prios[n], n) for n in names_prios), reverse=True))

View File

@@ -1,390 +0,0 @@
import os
import re
import subprocess
import sys
import time
from io import StringIO
from subprocess import PIPE
from typing import List, Tuple, Set, Dict, Optional, Iterable, Union, IO, Any
# default environment variables for subprocesses.
from bauh.api.abstract.handler import ProcessWatcher
PY_VERSION = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
GLOBAL_PY_LIBS = '/usr/lib/python{}'.format(PY_VERSION)
PATH = os.getenv('PATH')
DEFAULT_LANG = ''
GLOBAL_INTERPRETER_PATH = ':'.join(PATH.split(':')[1:])
if GLOBAL_PY_LIBS not in PATH:
PATH = '{}:{}'.format(GLOBAL_PY_LIBS, PATH)
USE_GLOBAL_INTERPRETER = bool(os.getenv('VIRTUAL_ENV'))
RE_SUDO_OUTPUT = re.compile(r'[sudo]\s*[\w\s]+:\s*')
def gen_env(global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: Optional[str] = DEFAULT_LANG,
extra_paths: Optional[Set[str]] = None) -> dict:
custom_env = dict(os.environ)
if lang is not None:
custom_env['LANG'] = lang
if global_interpreter: # to avoid subprocess calls to the virtualenv python interpreter instead of the global one.
custom_env['PATH'] = GLOBAL_INTERPRETER_PATH
else:
custom_env['PATH'] = PATH
if extra_paths:
custom_env['PATH'] = ':'.join(extra_paths) + ':' + custom_env['PATH']
return custom_env
class SystemProcess:
"""
Represents a system process being executed.
"""
def __init__(self, subproc: subprocess.Popen, success_phrases: List[str] = None, wrong_error_phrase: str = '[sudo] password for',
check_error_output: bool = True, skip_stdout: bool = False, output_delay: float = None):
self.subproc = subproc
self.success_phrases = success_phrases
self.wrong_error_phrase = wrong_error_phrase
self.check_error_output = check_error_output
self.skip_stdout = skip_stdout
self.output_delay = output_delay
def wait(self):
self.subproc.wait()
class SimpleProcess:
def __init__(self, cmd: Iterable[str], cwd: str = '.', expected_code: int = 0,
global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: Optional[str] = DEFAULT_LANG, root_password: Optional[str] = None,
extra_paths: Set[str] = None, error_phrases: Set[str] = None, wrong_error_phrases: Set[str] = None,
shell: bool = False, success_phrases: Set[str] = None, extra_env: Optional[Dict[str, str]] = None,
custom_user: Optional[str] = None, preserve_env: Optional[Set] = None):
pwdin, final_cmd = None, []
self.shell = shell
if custom_user:
final_cmd.extend(['runuser', '-u', custom_user, '--'])
elif isinstance(root_password, str):
final_cmd.extend(['sudo', '-S'])
if preserve_env:
for var in preserve_env:
final_cmd.append(f'--preserve-env={var}')
pwdin = self._new(['echo', root_password], cwd, global_interpreter, lang).stdout
final_cmd.extend(cmd)
self.instance = self._new(final_cmd, cwd, global_interpreter, lang=lang, stdin=pwdin,
extra_paths=extra_paths, extra_env=extra_env)
self.expected_code = expected_code
self.error_phrases = error_phrases
self.wrong_error_phrases = wrong_error_phrases
self.success_phrases = success_phrases
def _new(self, cmd: List[str], cwd: str, global_interpreter: bool, lang: Optional[str], stdin = None,
extra_paths: Set[str] = None, extra_env: Optional[Dict[str, str]] = None) -> subprocess.Popen:
env = gen_env(global_interpreter=global_interpreter, lang=lang, extra_paths=extra_paths)
if extra_env:
for var, val in extra_env.items():
env[var] = val
args = {
"stdout": subprocess.PIPE,
"stderr": subprocess.STDOUT,
"stdin": stdin if stdin else subprocess.DEVNULL,
"bufsize": -1,
"cwd": cwd,
"env": env,
"shell": self.shell
}
return subprocess.Popen(args=[' '.join(cmd)] if self.shell else cmd, **args)
class ProcessHandler:
"""
It handles a process execution and notifies a specified watcher.
"""
def __init__(self, watcher: ProcessWatcher = None):
self.watcher = watcher
def _notify_watcher(self, msg: str, as_substatus: bool = False):
if self.watcher:
self.watcher.print(msg)
if as_substatus:
self.watcher.change_substatus(msg)
def handle(self, process: SystemProcess, error_output: StringIO = None, output_handler=None) -> bool:
self._notify_watcher(' '.join(process.subproc.args) + '\n')
already_succeeded = False
if not process.skip_stdout:
for output in process.subproc.stdout:
try:
line = output.decode().strip()
except UnicodeDecodeError:
line = None
if line:
self._notify_watcher(line)
if output_handler:
output_handler(line)
if process.success_phrases and [p in line for p in process.success_phrases]:
already_succeeded = True
if not already_succeeded and process.output_delay:
time.sleep(process.output_delay)
if already_succeeded:
return True
for output in process.subproc.stderr:
if output:
try:
line = output.decode().strip()
except UnicodeDecodeError:
line = None
if line:
self._notify_watcher(line)
if output_handler:
output_handler(line)
if error_output is not None:
error_output.write(line)
if process.check_error_output:
if process.wrong_error_phrase and process.wrong_error_phrase in line:
continue
else:
return False
elif process.skip_stdout and process.success_phrases and [p in line for p in process.success_phrases]:
already_succeeded = True
if not already_succeeded and process.output_delay:
time.sleep(process.output_delay)
if already_succeeded:
return True
return process.subproc.returncode is None or process.subproc.returncode == 0
def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher: bool = True,
output_as_substatus: bool = False) -> Tuple[bool, str]:
if notify_watcher:
self._notify_watcher((proc.instance.args if isinstance(proc.instance.args, str) else ' '.join(proc.instance.args)) + '\n')
output = StringIO()
for o in proc.instance.stdout:
if o:
try:
line = o.decode()
except UnicodeDecodeError:
continue
if line.startswith('[sudo]'):
line = RE_SUDO_OUTPUT.split(line)[1]
output.write(line)
line = line.strip()
if line:
if output_handler:
output_handler(line)
if notify_watcher:
self._notify_watcher(line, as_substatus=output_as_substatus)
proc.instance.wait()
output.seek(0)
success = proc.instance.returncode == proc.expected_code
string_output = output.read()
if proc.success_phrases:
for phrase in proc.success_phrases:
if phrase in string_output:
success = True
break
if not success and proc.wrong_error_phrases:
for phrase in proc.wrong_error_phrases:
if phrase in string_output:
success = True
break
if success and proc.error_phrases:
for phrase in proc.error_phrases:
if phrase in string_output:
success = False
break
return success, string_output
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True,
cwd: str = '.', global_interpreter: bool = USE_GLOBAL_INTERPRETER, extra_paths: Set[str] = None,
custom_user: Optional[str] = None, lang: Optional[str] = DEFAULT_LANG) -> Optional[str]:
"""
runs a given command and returns its default output
:return:
"""
args = {
"shell": True,
"stdout": PIPE,
"env": gen_env(global_interpreter=global_interpreter, lang=lang, extra_paths=extra_paths),
'cwd': cwd
}
if not print_error:
args["stderr"] = subprocess.DEVNULL
final_cmd = f"runuser -u {custom_user} -- {cmd}" if custom_user else cmd
res = subprocess.run(final_cmd, **args)
if ignore_return_code or res.returncode == expected_code:
try:
return res.stdout.decode()
except UnicodeDecodeError:
pass
def new_subprocess(cmd: Iterable[str], cwd: str = '.', shell: bool = False, stdin: Optional[Union[None, int, IO[Any]]] = None,
global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: Optional[str] = DEFAULT_LANG,
extra_paths: Set[str] = None, custom_user: Optional[str] = None) -> subprocess.Popen:
args = {
"stdout": PIPE,
"stderr": PIPE,
"cwd": cwd,
"shell": shell,
"env": gen_env(global_interpreter, lang, extra_paths),
"stdin": stdin if stdin else subprocess.DEVNULL
}
final_cmd = ['runuser', '-u', custom_user, '--', *cmd] if custom_user else cmd
return subprocess.Popen(final_cmd, **args)
def new_root_subprocess(cmd: Iterable[str], root_password: Optional[str], cwd: str = '.',
global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG,
extra_paths: Set[str] = None, shell: bool = False) -> subprocess.Popen:
pwdin, final_cmd = subprocess.DEVNULL, []
if isinstance(root_password, str):
final_cmd.extend(['sudo', '-S'])
pwdin = new_subprocess(['echo', root_password], global_interpreter=global_interpreter, lang=lang).stdout
final_cmd.extend(cmd)
if shell:
final_cmd = ' '.join(final_cmd)
return subprocess.Popen(final_cmd, stdin=pwdin, stdout=PIPE, stderr=PIPE, cwd=cwd,
env=gen_env(global_interpreter, lang, extra_paths), shell=shell)
def notify_user(msg: str, app_name: str, icon_path: str):
os.system("notify-send -a {} {} '{}'".format(app_name, "-i {}".format(icon_path) if icon_path else '', msg))
def get_dir_size(start_path='.'):
total_size = 0
for dirpath, dirnames, filenames in os.walk(start_path):
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
return total_size
def run(cmd: List[str], success_code: int = 0, custom_user: Optional[str] = None) -> Tuple[bool, str]:
final_cmd = ['runuser', '-u', custom_user, '--', *cmd] if custom_user else cmd
p = subprocess.run(final_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
try:
output = p.stdout.decode()
except UnicodeDecodeError:
output = ''
return p.returncode == success_code, output
def check_active_services(*names: str) -> Dict[str, bool]:
output = run_cmd('systemctl is-active {}'.format(' '.join(names)), print_error=False)
if not output:
return {n: False for n in names}
else:
status = output.split('\n')
return {s: status[i].strip().lower() == 'active' for i, s in enumerate(names) if s}
def check_enabled_services(*names: str) -> Dict[str, bool]:
output = run_cmd('systemctl is-enabled {}'.format(' '.join(names)), print_error=False)
if not output:
return {n: False for n in names}
else:
status = output.split('\n')
return {s: status[i].strip().lower() == 'enabled' for i, s in enumerate(names) if s}
def execute(cmd: str, shell: bool = False, cwd: Optional[str] = None, output: bool = True, custom_env: Optional[dict] = None,
stdin: bool = True, custom_user: Optional[str] = None) -> Tuple[int, Optional[str]]:
final_cmd = f"runuser -u {custom_user} -- {cmd}" if custom_user else cmd
params = {
'args': final_cmd.split(' ') if not shell else [final_cmd],
'stdout': subprocess.PIPE if output else subprocess.DEVNULL,
'stderr': subprocess.STDOUT if output else subprocess.DEVNULL,
'shell': shell
}
if not stdin:
params['stdin'] = subprocess.DEVNULL
if cwd is not None:
params['cwd'] = cwd
if custom_env is not None:
params['env'] = custom_env
p = subprocess.run(**params)
output = None
if p.stdout:
try:
output = p.stdout.decode()
except UnicodeDecodeError:
output = None
return p.returncode, output

View File

@@ -1,91 +0,0 @@
import logging
import re
from abc import ABC
from datetime import datetime, timezone
from logging import Logger
from typing import Optional, Union
re_command_forbidden_symbols = re.compile(r'[\'\"%$#*<>]')
re_several_spaces = re.compile(r'\s+')
re_command_parameter = re.compile(r'(^|\s)-+\w+')
class NullLoggerFactory(ABC):
__instance: Optional[Logger] = None
@classmethod
def logger(cls) -> Logger:
if cls.__instance is None:
cls.__instance = logging.getLogger('__null__')
cls.__instance.addHandler(logging.NullHandler())
return cls.__instance
def deep_update(source: dict, overrides: dict):
for key, value in overrides.items():
if isinstance(value, dict):
returned = deep_update(source.get(key, {}), value)
source[key] = returned
else:
source[key] = overrides[key]
return source
def size_to_byte(size: Union[float, int, str], unit: str, logger: Optional[Logger] = None) -> Optional[float]:
lower_unit = unit.strip().lower()
if isinstance(size, str):
try:
final_size = float(size.strip().replace(',', '.').replace(' ', ''))
except ValueError:
if logger:
logger.error(f"Could not parse string size {size} to bytes")
return
else:
final_size = float(size)
if unit == 'b':
return final_size / 8
if unit == 'B':
return final_size
base = 1024 if lower_unit.endswith('ib') else 1000
if lower_unit[0] == 'k':
return final_size * base
elif lower_unit[0] == 'm':
return final_size * (base ** 2)
elif lower_unit[0] == 'g':
return final_size * (base ** 3)
elif lower_unit[0] == 't':
return final_size * (base ** 4)
else:
return final_size * (base ** 5)
def datetime_as_milis(date: Optional[datetime] = None) -> int:
if date is None:
date = datetime.now(timezone.utc)
return int(round(date.timestamp() * 1000))
def map_timestamp_file(file_path: str) -> str:
path_split = file_path.split('/')
return '/'.join(path_split[0:-1]) + '/' + path_split[-1].split('.')[0] + '.ts'
def sanitize_command_input(input_: str) -> str:
final_input = input_
for op in ('|', '&'):
final_input = final_input.split(op)[0]
for remove_re in (re_command_forbidden_symbols, re_command_parameter):
final_input = remove_re.sub('', final_input)
final_input = re_several_spaces.sub(' ', final_input)
return final_input.strip()

View File

@@ -1,67 +0,0 @@
import re
from typing import Tuple
RE_VERSION_WITH_EPOCH = re.compile(r'^\d+:(.+)$')
RE_VERSION_WITH_RELEASE = re.compile(r'^(.+)-\d+$')
def map_str_version(version: str) -> Tuple[str, ...]:
return tuple(part.zfill(8) for part in version.split("."))
def normalize_version(version: str) -> Tuple[int, Tuple[str, ...], int]:
raw_version = version.strip()
epoch = 0
if RE_VERSION_WITH_EPOCH.match(raw_version):
epoch_version = raw_version.split(":", maxsplit=1)
epoch = int(epoch_version[0])
raw_version = epoch_version[1]
release = 1
if RE_VERSION_WITH_RELEASE.match(raw_version):
version_release = raw_version.rsplit("-", maxsplit=1)
raw_version = version_release[0]
release = int(version_release[1])
if not raw_version.split(".")[0].isdigit():
# this is required to properly compare versions starting with a number against versions starting with alpha
raw_version = f"0.{raw_version}"
return epoch, map_str_version(raw_version), release
def match_required_version(current_version: str, operator: str, required_version: str) -> bool:
final_required, final_current = required_version.strip(), current_version.strip()
required_has_epoch = bool(RE_VERSION_WITH_EPOCH.match(final_required))
current_no_epoch = RE_VERSION_WITH_EPOCH.split(final_current)
current_has_epoch = len(current_no_epoch) > 1
if required_has_epoch and not current_has_epoch:
final_current = f'0:{final_current}'
elif current_has_epoch and not required_has_epoch:
final_current = current_no_epoch[1]
required_has_release = bool(RE_VERSION_WITH_RELEASE.match(final_required))
current_no_release = RE_VERSION_WITH_RELEASE.split(final_current)
current_has_release = len(current_no_release) > 1
if required_has_release and not current_has_release:
final_current = f'{final_current}-1'
elif current_has_release and not required_has_release:
final_current = current_no_release[1]
final_required, final_current = map_str_version(final_required), map_str_version(final_current)
if operator == '==' or operator == '=':
return final_current == final_required
elif operator == '>':
return final_current > final_required
elif operator == '>=':
return final_current >= final_required
elif operator == '<':
return final_current < final_required
elif operator == '<=':
return final_current <= final_required

View File

@@ -1,36 +0,0 @@
import locale
from typing import Tuple, Optional, Iterable
from bauh.api.abstract.view import SelectViewType, InputOption, SingleSelectComponent
def new_select(label: str, tip: Optional[str], id_: str, opts: Iterable[Tuple[Optional[str], object, Optional[str]]], value: object, max_width: Optional[int] = None,
type_: SelectViewType = SelectViewType.RADIO, capitalize_label: bool = True):
inp_opts = [InputOption(label=o[0].capitalize(), value=o[1], tooltip=o[2]) for o in opts]
def_opt = [o for o in inp_opts if o.value == value]
return SingleSelectComponent(label=label,
tooltip=tip,
options=inp_opts,
default_option=def_opt[0] if def_opt else inp_opts[0],
max_per_line=len(inp_opts),
max_width=max_width,
type_=type_,
id_=id_,
capitalize_label=capitalize_label)
def get_human_size_str(size, positive_sign: bool = False) -> Optional[str]:
if type(size) in (int, float, str):
int_size = abs(int(size))
if int_size == 0:
return '0'
for power, unit in enumerate(('B', 'kB', 'MB', 'GB', 'TB', 'PB')):
size_unit = int_size / (1000 ** power)
if size_unit < 1000:
size_unit = size_unit if size > 0 else size_unit * -1
localized_size = locale.format_string('%.2f', size_unit)
size_str = f'{int(size_unit)} {unit}' if unit == 'B' else f"{localized_size} {unit}"
return f'+{size_str}' if positive_sign and size_unit > 0 else size_str