Modernize runtime compatibility and initialize Bearhub fork

This commit is contained in:
Sebastian Palencsar
2026-05-26 08:35:29 +02:00
parent b1ea479a3a
commit d541a4cbcf
23 changed files with 132 additions and 96 deletions

View File

@@ -2,7 +2,7 @@ import logging
import os
import time
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from threading import Thread
from typing import Dict, List, Optional
@@ -93,7 +93,7 @@ class CategoriesDownloader(Thread):
self.logger.info(self._msg('Downloading category definitions from {}'.format(self.url_categories_file)))
try:
timestamp = datetime.utcnow().timestamp()
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_)))
@@ -139,13 +139,13 @@ class CategoriesDownloader(Thread):
timestamp_str = f.read()
try:
categories_timestamp = datetime.fromtimestamp(float(timestamp_str))
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.utcnow())
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)))

View File

@@ -1,7 +1,7 @@
import logging
import re
from abc import ABC
from datetime import datetime
from datetime import datetime, timezone
from logging import Logger
from typing import Optional, Union
@@ -66,7 +66,10 @@ def size_to_byte(size: Union[float, int, str], unit: str, logger: Optional[Logge
return final_size * (base ** 5)
def datetime_as_milis(date: datetime = datetime.utcnow()) -> int:
def datetime_as_milis(date: Optional[datetime] = None) -> int:
if date is None:
date = datetime.now(timezone.utc)
return int(round(date.timestamp() * 1000))

View File

@@ -1,6 +1,6 @@
[Desktop Entry]
Type=Application
Name=Applications (bauh)
Name=Applications (Bearhub)
Name[pt]=Aplicativos (bauh)
Name[es]=Aplicaciones (bauh)
Name[ca]=Aplicacions (bauh)
@@ -17,5 +17,5 @@ Comment[de]=Anwendungen installieren und entfernen (AppImage, Arch, Flatpak, Sna
Comment[ca]=Instal·lar i eliminar aplicacions (AppImage, Arch, Flatpak, Snap, Web)
Comment[ru]=Установка и удаление приложений (AppImage, Arch, Flatpak, Snap, Web)
Comment[tr]=Uygulama yükle/kaldır (AppImage, Arch, Flatpak, Snap, Web)
Exec=/usr/bin/bauh
Exec=/usr/bin/bearhub
Icon=bauh

View File

@@ -1,6 +1,6 @@
[Desktop Entry]
Type=Application
Name=Applications (bauh-tray)
Name=Applications (Bearhub tray)
Name[pt]=Aplicativos (bauh-bandeja)
Name[es]=Aplicaciones (bauh-bandeja)
Name[ca]=Aplicacions (bauh-safata)
@@ -17,5 +17,5 @@ Comment[de]=Anwendungen installieren und entfernen (AppImage, Arch, Flatpak, Sna
Comment[ca]=Instal·lar i eliminar aplicacions (AppImage, Arch, Flatpak, Snap, Web)
Comment[ru]=Установка и удаление приложений (AppImage, Arch, Flatpak, Snap, Web)
Comment[tr]=Uygulama yükle/kaldır (AppImage, Arch, Flatpak, Snap, Web)
Exec=/usr/bin/bauh-tray
Exec=/usr/bin/bearhub-tray
Icon=bauh

View File

@@ -5,7 +5,7 @@ import os
import tarfile
import time
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from threading import Thread
from typing import Optional, Generator
@@ -73,13 +73,13 @@ class DatabaseUpdater(Thread):
dbs_ts_str = f.read()
try:
dbs_timestamp = datetime.fromtimestamp(float(dbs_ts_str))
dbs_timestamp = datetime.fromtimestamp(float(dbs_ts_str), tz=timezone.utc)
except Exception:
self.logger.error('Could not parse the databases timestamp: {}'.format(dbs_ts_str))
traceback.print_exc()
return True
update = dbs_timestamp + timedelta(minutes=db_exp) <= datetime.utcnow()
update = dbs_timestamp + timedelta(minutes=db_exp) <= datetime.now(timezone.utc)
self.logger.info('Finished. Took {0:.2f} seconds'.format(time.time() - ti))
return update
@@ -93,7 +93,7 @@ class DatabaseUpdater(Thread):
self._update_task_progress(10, self.i18n['appimage.update_database.downloading'])
self.logger.info('Retrieving AppImage databases')
database_timestamp = datetime.utcnow().timestamp()
database_timestamp = datetime.now(timezone.utc).timestamp()
try:
res = self.http_client.get(URL_COMPRESSED_DATABASES, session=False)
except Exception as e:
@@ -356,13 +356,13 @@ class AppImageSuggestionsDownloader(Thread):
timestamp_str = f.read()
try:
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str))
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception:
self.logger.error(f'Could not parse the cached AppImage suggestions timestamp: {timestamp_str}')
traceback.print_exc()
return True
update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.utcnow()
update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.now(timezone.utc)
return update
def read(self) -> Generator[str, None, None]:
@@ -372,7 +372,7 @@ class AppImageSuggestionsDownloader(Thread):
self.logger.info("Checking if AppImage suggestions should be downloaded")
if self.should_download(self.config):
suggestions_timestamp = datetime.utcnow().timestamp()
suggestions_timestamp = datetime.now(timezone.utc).timestamp()
suggestions_str = self.download()
Thread(target=self.cache_suggestions, args=(suggestions_str, suggestions_timestamp), daemon=True).start()
@@ -466,7 +466,7 @@ class AppImageSuggestionsDownloader(Thread):
try:
if should_download:
suggestions_timestamp = datetime.utcnow().timestamp()
suggestions_timestamp = datetime.now(timezone.utc).timestamp()
suggestions_str = self.download()
self.taskman.update_progress(self.task_id, 70, None)

View File

@@ -290,7 +290,7 @@ def read_repository_from_info(name: str) -> Optional[str]:
repository = None
for o in new_subprocess(['grep', '-Po', "Repository\s+:\s+\K.+"], stdin=info.stdout).stdout:
for o in new_subprocess(['grep', '-Po', r"Repository\s+:\s+\K.+"], stdin=info.stdout).stdout:
if o:
line = o.decode().strip()
@@ -343,7 +343,7 @@ def read_provides(name: str) -> Set[str]:
provides = None
for out in new_subprocess(['grep', '-Po', 'Provides\s+:\s\K(.+)'], stdin=dep_info.stdout).stdout:
for out in new_subprocess(['grep', '-Po', r'Provides\s+:\s\K(.+)'], stdin=dep_info.stdout).stdout:
if out:
provided_names = [p.strip() for p in out.decode().strip().split(' ') if p]
@@ -372,7 +372,7 @@ def read_dependencies(name: str) -> Set[str]:
raise PackageNotFoundException(name)
depends_on = set()
for out in new_subprocess(['grep', '-Po', 'Depends\s+On\s+:\s\K(.+)'], stdin=dep_info.stdout).stdout:
for out in new_subprocess(['grep', '-Po', r'Depends\s+On\s+:\s\K(.+)'], stdin=dep_info.stdout).stdout:
if out:
line = out.decode().strip()

View File

@@ -1,6 +1,6 @@
import os
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from logging import Logger
from pathlib import Path
from threading import Thread
@@ -90,13 +90,13 @@ class RepositorySuggestionsDownloader(Thread):
timestamp_str = f.read()
try:
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str))
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception:
self._log.error(f'Could not parse the Arch cached suggestions timestamp: {timestamp_str}')
traceback.print_exc()
return True
update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.utcnow()
update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.now(timezone.utc)
if update:
self._log.info("The cached suggestions file is no longer valid")
@@ -166,7 +166,7 @@ class RepositorySuggestionsDownloader(Thread):
suggestions = parse(res.text, self._log, 'Arch')
if suggestions:
self._save(text=res.text, timestamp=datetime.utcnow().timestamp())
self._save(text=res.text, timestamp=datetime.now(timezone.utc).timestamp())
else:
self._log.warning(f"Could not parse any Arch suggestion from {self._file_suggestions_ts}")
else:

View File

@@ -6,7 +6,7 @@ import re
import shutil
import time
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from threading import Thread
from typing import Optional
@@ -71,8 +71,8 @@ class AURIndexUpdater(Thread):
timestamp_str = f.read()
try:
index_timestamp = datetime.fromtimestamp(float(timestamp_str))
return (index_timestamp + timedelta(hours=exp_hours)) <= datetime.utcnow()
index_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
return (index_timestamp + timedelta(hours=exp_hours)) <= datetime.now(timezone.utc)
except Exception:
traceback.print_exc()
return True
@@ -81,7 +81,7 @@ class AURIndexUpdater(Thread):
self.logger.info('Indexing AUR packages')
self.taskman.update_progress(self.task_id, 5, self.i18n['arch.task.aur.index.substatus.download'])
try:
index_ts = datetime.utcnow().timestamp()
index_ts = datetime.now(timezone.utc).timestamp()
res = self.http_client.get(URL_INDEX)
if res and res.text:

View File

@@ -3,7 +3,7 @@ import os
import re
import traceback
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from json import JSONDecodeError
from logging import Logger
from pathlib import Path
@@ -52,14 +52,14 @@ class ApplicationIndexer:
return True
try:
timestamp = datetime.fromtimestamp(float(timestamp_str))
timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception:
self._log.error(f'Could not parse the Debian applications index timestamp: {timestamp_str} '
f'({self._file_timestamp_path})')
traceback.print_exc()
return True
expired = timestamp + timedelta(minutes=exp_minutes) <= datetime.utcnow()
expired = timestamp + timedelta(minutes=exp_minutes) <= datetime.now(timezone.utc)
if expired:
self._log.info("Debian applications index has expired. A new one must be generated.")
@@ -122,7 +122,7 @@ class ApplicationIndexer:
raise ApplicationIndexError()
if update_timestamp:
index_timestamp = datetime.utcnow().timestamp()
index_timestamp = datetime.now(timezone.utc).timestamp()
try:
with open(self._file_timestamp_path, 'w+') as f:
f.write(str(index_timestamp))
@@ -140,7 +140,7 @@ class ApplicationsMapper:
def __init__(self, logger: Logger, workers: int = 10):
self._log = logger
self._re_desktop_file = re.compile(r'(.+):\s+(/usr/share/applications/.+\.desktop)')
self._re_desktop_file_fields = re.compile('(Exec|TryExec|Icon|Categories|NoDisplay|Terminal)\s*=\s*(.+)')
self._re_desktop_file_fields = re.compile(r'(Exec|TryExec|Icon|Categories|NoDisplay|Terminal)\s*=\s*(.+)')
self._workers = workers
def _read_file(self, file_path: str) -> Optional[str]:

View File

@@ -1,6 +1,6 @@
import os
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from logging import Logger
from pathlib import Path
from threading import Thread
@@ -95,13 +95,13 @@ class DebianSuggestionsDownloader(Thread):
timestamp_str = f.read()
try:
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str))
suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception:
self._log.error(f'Could not parse the Debian cached suggestions timestamp: {timestamp_str}')
traceback.print_exc()
return True
update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.utcnow()
update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.now(timezone.utc)
return update
def _save(self, text: str, timestamp: float):
@@ -184,7 +184,7 @@ class DebianSuggestionsDownloader(Thread):
suggestions = parse(res.text, self._log, 'Debian')
if suggestions:
self._save(text=res.text, timestamp=datetime.utcnow().timestamp())
self._save(text=res.text, timestamp=datetime.now(timezone.utc).timestamp())
else:
self._log.warning("No Debian suggestions to cache")
else:

View File

@@ -1,6 +1,6 @@
import time
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from logging import Logger
from threading import Thread
from typing import Optional, Set
@@ -149,14 +149,14 @@ class SynchronizePackages(Thread):
return True
try:
last_timestamp = datetime.fromtimestamp(float(timestamp_str))
last_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception:
logger.error(f'Could not parse the packages synchronization timestamp: {timestamp_str} '
f'({PACKAGE_SYNC_TIMESTAMP_FILE})')
traceback.print_exc()
return True
expired = last_timestamp + timedelta(minutes=period) <= datetime.utcnow()
expired = last_timestamp + timedelta(minutes=period) <= datetime.now(timezone.utc)
if expired:
logger.info("Packages synchronization is outdated")
@@ -176,7 +176,7 @@ class SynchronizePackages(Thread):
self._taskman.update_progress(self._id, 99, None)
if updated:
index_timestamp = datetime.utcnow().timestamp()
index_timestamp = datetime.now(timezone.utc).timestamp()
finish_msg = None
try:
with open(PACKAGE_SYNC_TIMESTAMP_FILE, 'w+') as f:

View File

@@ -1,7 +1,7 @@
import os
import re
import traceback
from datetime import datetime
from datetime import datetime, timezone
from operator import attrgetter
from pathlib import Path
from threading import Thread
@@ -60,7 +60,7 @@ class FlatpakManager(SoftwareManager, SettingsController):
if app.runtime and app.latest_version is None:
app.latest_version = app.version
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.now(timezone.utc)
data_loader: Optional[FlatpakAsyncDataLoader] = None

View File

@@ -16,7 +16,7 @@ from bauh.gems.flatpak.constants import FLATHUB_URL
RE_SEVERAL_SPACES = re.compile(r'\s+')
RE_COMMIT = re.compile(r'(Latest commit|Commit)\s*:\s*(.+)')
RE_REQUIRED_RUNTIME = re.compile(f'Required\s+runtime\s+.+\(([\w./]+)\)\s*.+\s+remote\s+([\w+./]+)')
RE_REQUIRED_RUNTIME = re.compile(r'Required\s+runtime\s+.+\(([\w./]+)\)\s*.+\s+remote\s+([\w+./]+)')
OPERATION_UPDATE_SYMBOLS = {'i', 'u'}
@@ -56,12 +56,20 @@ def get_fields(app_id: str, branch: str, fields: List[str]) -> List[str]:
if branch:
cmd.append(branch)
info = new_subprocess(cmd).stdout
info = run_cmd(' '.join(cmd), print_error=False)
if not info:
return []
res = []
for o in new_subprocess(('grep', '-E', '({}):.+'.format('|'.join(fields)), '-o'), stdin=info).stdout:
if o:
res.append(o.decode().split(':')[-1].strip())
for line in info.splitlines():
if not line or ':' not in line:
continue
field_name, field_value = line.split(':', 1)
if field_name.strip() in fields:
res.append(field_value.strip())
return res
@@ -97,11 +105,14 @@ def list_installed(version: Tuple[str, ...]) -> List[dict]:
apps = []
if version < VERSION_1_2:
app_list = new_subprocess(('flatpak', 'list', '-d'), lang=None)
app_list = run_cmd('flatpak list -d', print_error=False, lang=None)
for o in app_list.stdout:
if o:
data = o.decode().strip().split('\t')
if not app_list:
return apps
for line in app_list.splitlines():
if line:
data = line.strip().split('\t')
ref_split = data[0].split('/')
runtime = 'runtime' in data[5]
@@ -121,11 +132,14 @@ def list_installed(version: Tuple[str, ...]) -> List[dict]:
else:
name_col = '' if version < VERSION_1_3 else 'name,'
cols = f'application,ref,arch,branch,description,origin,options,{name_col}version'
app_list = new_subprocess(('flatpak', 'list', f'--columns={cols}'), lang=None)
app_list = run_cmd(f'flatpak list --columns={cols}', print_error=False, lang=None)
for o in app_list.stdout:
if o:
data = o.decode().strip().split('\t')
if not app_list:
return apps
for line in app_list.splitlines():
if line:
data = line.strip().split('\t')
runtime = 'runtime' in data[6]
if version < VERSION_1_3:
@@ -237,14 +251,17 @@ def fill_updates(version: Tuple[str, ...], installation: str, res: Dict[str, Set
except Exception:
traceback.print_exc()
else:
updates = new_subprocess(('flatpak', 'update', f'--{installation}', '--no-deps')).stdout
reg = r'[0-9]+\.\s+.+'
try:
for o in new_subprocess(('grep', '-E', reg, '-o', '--color=never'), stdin=updates).stdout:
if o:
line_split = o.decode().strip().split('\t')
output = run_cmd(f'flatpak update --{installation} --no-deps', ignore_return_code=True, print_error=False)
if not output:
return
for line in output.splitlines():
if re.match(reg, line):
line_split = line.strip().split('\t')
if len(line_split) >= 5:
if version >= VERSION_1_5:

View File

@@ -81,7 +81,7 @@ class WebApplicationManager(SoftwareManager, SettingsController):
def get_accept_language_header(self) -> str:
try:
syslocale = locale.getdefaultlocale()
syslocale = locale.getlocale()
if syslocale:
locale_split = syslocale[0].split('_')

View File

@@ -4,7 +4,7 @@ import os
import shutil
import tarfile
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from threading import Thread
from typing import Dict, List, Optional
@@ -245,12 +245,12 @@ class EnvironmentUpdater:
env_ts_str = f.read()
try:
env_timestamp = datetime.fromtimestamp(float(env_ts_str))
env_timestamp = datetime.fromtimestamp(float(env_ts_str), tz=timezone.utc)
except Exception:
self.logger.error(f"Could not parse environment settings file timestamp: {env_ts_str}")
return True
expired = env_timestamp + timedelta(hours=settings_exp) <= datetime.utcnow()
expired = env_timestamp + timedelta(hours=settings_exp) <= datetime.now(timezone.utc)
if expired:
self.logger.info("Environment settings file has expired. It should be re-downloaded")
@@ -314,7 +314,7 @@ class EnvironmentUpdater:
self._finish_task_download_settings()
return
cache_timestamp = datetime.utcnow().timestamp()
cache_timestamp = datetime.now(timezone.utc).timestamp()
with open(ENVIRONMENT_SETTINGS_CACHED_FILE, 'w+') as f:
f.write(yaml.safe_dump(settings))

View File

@@ -1,6 +1,6 @@
import os
import traceback
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from logging import Logger
from pathlib import Path
from typing import Optional
@@ -68,12 +68,12 @@ class SuggestionsManager:
timestamp_str = f.read()
try:
sugs_timestamp = datetime.fromtimestamp(float(timestamp_str))
sugs_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception:
self.logger.error(f"Could not parse the cached suggestions file timestamp: {timestamp_str}")
return True
expired = sugs_timestamp + timedelta(days=exp) <= datetime.utcnow()
expired = sugs_timestamp + timedelta(days=exp) <= datetime.now(timezone.utc)
if expired:
self.logger.info("Cached suggestions file has expired.")

View File

@@ -1,7 +1,7 @@
import logging
import time
import traceback
from datetime import datetime
from datetime import datetime, timezone
from threading import Thread
from typing import Optional
@@ -53,7 +53,7 @@ class SuggestionsLoader(Thread):
self.suggestions = self.manager.read_cached(check_file=False)
else:
try:
timestamp = datetime.utcnow().timestamp()
timestamp = datetime.now(timezone.utc).timestamp()
self.suggestions = self.manager.download()
if self.suggestions:

View File

@@ -1,6 +1,6 @@
import inspect
import importlib
import os
import pkgutil
from logging import Logger
from typing import List, Generator
@@ -49,11 +49,14 @@ def load_managers(locale: str, context: ApplicationContext, config: dict, defaul
logger.warning(f"gem '{f.name}' could not be loaded because it was marked as forbidden in '{FORBIDDEN_GEMS_FILE}'")
continue
loader = pkgutil.find_loader(f'bauh.gems.{f.name}.controller')
module_name = f'bauh.gems.{f.name}.controller'
if loader:
module = loader.load_module()
try:
module = importlib.import_module(module_name)
except ModuleNotFoundError:
module = None
if module:
manager_class = find_manager(module)
if manager_class:

View File

@@ -27,7 +27,7 @@ class DefaultMemoryCache(MemoryCache):
def _add(self, key: str, val: object):
if key:
self._cache[key] = {'val': val, 'expires_at': datetime.datetime.utcnow() + datetime.timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None}
self._cache[key] = {'val': val, 'expires_at': datetime.datetime.now(UTC) + datetime.timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None}
def add_non_existing(self, key: str, val: object):
if key and self. is_enabled():
@@ -46,7 +46,7 @@ class DefaultMemoryCache(MemoryCache):
if val:
expiration = val.get('expires_at')
if expiration and expiration <= datetime.datetime.utcnow():
if expiration and expiration <= datetime.datetime.now(UTC):
if lock:
self.lock.acquire()
@@ -86,3 +86,4 @@ class DefaultMemoryCacheFactory(MemoryCacheFactory):
def new(self, expiration: Optional[int] = None) -> MemoryCache:
return DefaultMemoryCache(expiration if expiration is not None else self.expiration_time)
UTC = datetime.timezone.utc

View File

@@ -50,7 +50,7 @@ def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale
if key is None:
try:
current_locale = locale.getdefaultlocale()
current_locale = locale.getlocale()
if current_locale is None or current_locale[0] is None:
current_locale = ('en', 'UTF-8')