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

@@ -1,4 +1,12 @@
[![GitHub release](https://img.shields.io/github/release/vinifmor/bauh.svg?label=Release)](https://github.com/vinifmor/bauh/releases/) [![PyPI](https://img.shields.io/pypi/v/bauh?label=PyPI)](https://pypi.org/project/bauh) [![AUR](https://img.shields.io/aur/version/bauh?label=AUR)](https://aur.archlinux.org/packages/bauh) [![AUR-staging](https://img.shields.io/aur/version/bauh-staging?label=AUR-staging)](https://aur.archlinux.org/packages/bauh-staging) [![License](https://img.shields.io/github/license/vinifmor/bauh?label=License)](https://github.com/vinifmor/bauh/blob/master/LICENSE) [![kofi](https://img.shields.io/badge/Ko--Fi-Donate-orange?style=flat&logo=ko-fi)](https://ko-fi.com/vinifmor) [![License](https://img.shields.io/github/license/spalencsar/bearhub?label=License)](https://github.com/spalencsar/bearhub/blob/master/LICENSE)
# Bearhub
**Bearhub** is a community-maintained fork of [bauh](https://github.com/vinifmor/bauh), focused on keeping the application working on current Linux systems, especially modern Arch Linux and Python versions.
The original project was created by Vinicius Moreira. This fork continues from that codebase under the original license and keeps the history and authorship intact.
At the moment, parts of the documentation below still refer to the original `bauh` command name, paths, package names, and screenshots. Those references are historical and will be updated incrementally as the fork evolves.
**bauh** (baoo), formerly known as [fpakman](https://github.com/vinifmor/fpakman), is a graphical interface for managing your Linux software (packages/applications). It currently supports the following formats: AppImage, Debian and Arch Linux packages (including AUR), Flatpak, Snap and Web applications. **bauh** (baoo), formerly known as [fpakman](https://github.com/vinifmor/fpakman), is a graphical interface for managing your Linux software (packages/applications). It currently supports the following formats: AppImage, Debian and Arch Linux packages (including AUR), Flatpak, Snap and Web applications.

View File

@@ -2,7 +2,7 @@ import logging
import os import os
import time import time
import traceback import traceback
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from threading import Thread from threading import Thread
from typing import Dict, List, Optional 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))) self.logger.info(self._msg('Downloading category definitions from {}'.format(self.url_categories_file)))
try: try:
timestamp = datetime.utcnow().timestamp() timestamp = datetime.now(timezone.utc).timestamp()
res = self.http_client.get(self.url_categories_file) res = self.http_client.get(self.url_categories_file)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
self.logger.error(self._msg('[{}] Could not download categories. The internet connection seems to be off.'.format(self.id_))) 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() timestamp_str = f.read()
try: try:
categories_timestamp = datetime.fromtimestamp(float(timestamp_str)) categories_timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
except Exception: 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))) 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() traceback.print_exc()
return True 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: if should_download:
self.logger.info(self._msg("Cached categories file '{}' has expired. A new one should be downloaded.".format(self.categories_path))) 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 logging
import re import re
from abc import ABC from abc import ABC
from datetime import datetime from datetime import datetime, timezone
from logging import Logger from logging import Logger
from typing import Optional, Union 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) 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)) return int(round(date.timestamp() * 1000))

View File

@@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Applications (bauh) Name=Applications (Bearhub)
Name[pt]=Aplicativos (bauh) Name[pt]=Aplicativos (bauh)
Name[es]=Aplicaciones (bauh) Name[es]=Aplicaciones (bauh)
Name[ca]=Aplicacions (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[ca]=Instal·lar i eliminar aplicacions (AppImage, Arch, Flatpak, Snap, Web)
Comment[ru]=Установка и удаление приложений (AppImage, Arch, Flatpak, Snap, Web) Comment[ru]=Установка и удаление приложений (AppImage, Arch, Flatpak, Snap, Web)
Comment[tr]=Uygulama yükle/kaldır (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 Icon=bauh

View File

@@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Applications (bauh-tray) Name=Applications (Bearhub tray)
Name[pt]=Aplicativos (bauh-bandeja) Name[pt]=Aplicativos (bauh-bandeja)
Name[es]=Aplicaciones (bauh-bandeja) Name[es]=Aplicaciones (bauh-bandeja)
Name[ca]=Aplicacions (bauh-safata) 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[ca]=Instal·lar i eliminar aplicacions (AppImage, Arch, Flatpak, Snap, Web)
Comment[ru]=Установка и удаление приложений (AppImage, Arch, Flatpak, Snap, Web) Comment[ru]=Установка и удаление приложений (AppImage, Arch, Flatpak, Snap, Web)
Comment[tr]=Uygulama yükle/kaldır (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 Icon=bauh

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import os import os
import re import re
import traceback import traceback
from datetime import datetime from datetime import datetime, timezone
from operator import attrgetter from operator import attrgetter
from pathlib import Path from pathlib import Path
from threading import Thread from threading import Thread
@@ -60,7 +60,7 @@ class FlatpakManager(SoftwareManager, SettingsController):
if app.runtime and app.latest_version is None: if app.runtime and app.latest_version is None:
app.latest_version = app.version 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 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_SEVERAL_SPACES = re.compile(r'\s+')
RE_COMMIT = re.compile(r'(Latest commit|Commit)\s*:\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'} OPERATION_UPDATE_SYMBOLS = {'i', 'u'}
@@ -56,12 +56,20 @@ def get_fields(app_id: str, branch: str, fields: List[str]) -> List[str]:
if branch: if branch:
cmd.append(branch) cmd.append(branch)
info = new_subprocess(cmd).stdout info = run_cmd(' '.join(cmd), print_error=False)
if not info:
return []
res = [] res = []
for o in new_subprocess(('grep', '-E', '({}):.+'.format('|'.join(fields)), '-o'), stdin=info).stdout: for line in info.splitlines():
if o: if not line or ':' not in line:
res.append(o.decode().split(':')[-1].strip()) continue
field_name, field_value = line.split(':', 1)
if field_name.strip() in fields:
res.append(field_value.strip())
return res return res
@@ -97,11 +105,14 @@ def list_installed(version: Tuple[str, ...]) -> List[dict]:
apps = [] apps = []
if version < VERSION_1_2: 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 not app_list:
if o: return apps
data = o.decode().strip().split('\t')
for line in app_list.splitlines():
if line:
data = line.strip().split('\t')
ref_split = data[0].split('/') ref_split = data[0].split('/')
runtime = 'runtime' in data[5] runtime = 'runtime' in data[5]
@@ -121,11 +132,14 @@ def list_installed(version: Tuple[str, ...]) -> List[dict]:
else: else:
name_col = '' if version < VERSION_1_3 else 'name,' name_col = '' if version < VERSION_1_3 else 'name,'
cols = f'application,ref,arch,branch,description,origin,options,{name_col}version' 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 not app_list:
if o: return apps
data = o.decode().strip().split('\t')
for line in app_list.splitlines():
if line:
data = line.strip().split('\t')
runtime = 'runtime' in data[6] runtime = 'runtime' in data[6]
if version < VERSION_1_3: if version < VERSION_1_3:
@@ -237,14 +251,17 @@ def fill_updates(version: Tuple[str, ...], installation: str, res: Dict[str, Set
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
else: else:
updates = new_subprocess(('flatpak', 'update', f'--{installation}', '--no-deps')).stdout
reg = r'[0-9]+\.\s+.+' reg = r'[0-9]+\.\s+.+'
try: try:
for o in new_subprocess(('grep', '-E', reg, '-o', '--color=never'), stdin=updates).stdout: output = run_cmd(f'flatpak update --{installation} --no-deps', ignore_return_code=True, print_error=False)
if o:
line_split = o.decode().strip().split('\t') 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 len(line_split) >= 5:
if version >= VERSION_1_5: if version >= VERSION_1_5:

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import inspect import inspect
import importlib
import os import os
import pkgutil
from logging import Logger from logging import Logger
from typing import List, Generator 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}'") logger.warning(f"gem '{f.name}' could not be loaded because it was marked as forbidden in '{FORBIDDEN_GEMS_FILE}'")
continue continue
loader = pkgutil.find_loader(f'bauh.gems.{f.name}.controller') module_name = f'bauh.gems.{f.name}.controller'
if loader: try:
module = loader.load_module() module = importlib.import_module(module_name)
except ModuleNotFoundError:
module = None
if module:
manager_class = find_manager(module) manager_class = find_manager(module)
if manager_class: if manager_class:

View File

@@ -27,7 +27,7 @@ class DefaultMemoryCache(MemoryCache):
def _add(self, key: str, val: object): def _add(self, key: str, val: object):
if key: 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): def add_non_existing(self, key: str, val: object):
if key and self. is_enabled(): if key and self. is_enabled():
@@ -46,7 +46,7 @@ class DefaultMemoryCache(MemoryCache):
if val: if val:
expiration = val.get('expires_at') expiration = val.get('expires_at')
if expiration and expiration <= datetime.datetime.utcnow(): if expiration and expiration <= datetime.datetime.now(UTC):
if lock: if lock:
self.lock.acquire() self.lock.acquire()
@@ -86,3 +86,4 @@ class DefaultMemoryCacheFactory(MemoryCacheFactory):
def new(self, expiration: Optional[int] = None) -> MemoryCache: def new(self, expiration: Optional[int] = None) -> MemoryCache:
return DefaultMemoryCache(expiration if expiration is not None else self.expiration_time) 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: if key is None:
try: try:
current_locale = locale.getdefaultlocale() current_locale = locale.getlocale()
if current_locale is None or current_locale[0] is None: if current_locale is None or current_locale[0] is None:
current_locale = ('en', 'UTF-8') current_locale = ('en', 'UTF-8')

View File

@@ -3,13 +3,16 @@ requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "bauh" name = "bearhub"
description = "Graphical interface to manage Linux applications (AppImage, Arch / AUR, Flatpak, Snap and Web)" description = "Community-maintained fork of bauh focused on modern Linux compatibility"
license = {file = "LICENSE"} license = {file = "LICENSE"}
requires-python = ">=3.6" requires-python = ">=3.6"
dynamic = ["version"] dynamic = ["version"]
readme = "README.md" readme = "README.md"
authors = [{name = "Vinicius Moreira", email = "vinicius_fmoreira@hotmail.com"}] authors = [
{name = "Vinicius Moreira", email = "vinicius_fmoreira@hotmail.com"},
{name = "Bearhub maintainers"}
]
classifiers = [ classifiers = [
'Topic :: Utilities', 'Topic :: Utilities',
'Programming Language :: Python', 'Programming Language :: Python',
@@ -36,12 +39,12 @@ web = [
] ]
[project.scripts] [project.scripts]
bauh = "bauh.app:main" bearhub = "bauh.app:main"
bauh-tray = "bauh.app:tray" bearhub-tray = "bauh.app:tray"
bauh-cli = "bauh.cli.app:main" bearhub-cli = "bauh.cli.app:main"
[project.urls] [project.urls]
Repository = "https://github.com/vinifmor/bauh" Repository = "https://github.com/spalencsar/bearhub"
[tool.setuptools] [tool.setuptools]
license-files = ["LICENSE"] license-files = ["LICENSE"]

View File

@@ -4,13 +4,14 @@ import os
from setuptools import setup, find_packages from setuptools import setup, find_packages
DESCRIPTION = ( DESCRIPTION = (
"Graphical interface to manage Linux applications (AppImage, Arch / AUR, Flatpak, Snap and Web)" "Community-maintained fork of bauh focused on modern Linux compatibility"
) )
AUTHOR = "Vinicius Moreira" AUTHOR = "Vinicius Moreira"
AUTHOR_EMAIL = "vinicius_fmoreira@hotmail.com" AUTHOR_EMAIL = "vinicius_fmoreira@hotmail.com"
NAME = 'bauh' DIST_NAME = 'bearhub'
URL = "https://github.com/vinifmor/" + NAME APP_PACKAGE = 'bauh'
URL = "https://github.com/spalencsar/" + DIST_NAME
file_dir = os.path.dirname(os.path.abspath(__file__)) file_dir = os.path.dirname(os.path.abspath(__file__))
@@ -21,12 +22,12 @@ else:
requirements = [line.strip() for line in f.readlines() if line] requirements = [line.strip() for line in f.readlines() if line]
with open(file_dir + '/{}/__init__.py'.format(NAME), 'r') as f: with open(file_dir + '/{}/__init__.py'.format(APP_PACKAGE), 'r') as f:
exec(f.readlines()[0]) exec(f.readlines()[0])
setup( setup(
name=NAME, name=DIST_NAME,
version=eval('__version__'), version=eval('__version__'),
description=DESCRIPTION, description=DESCRIPTION,
long_description=DESCRIPTION, long_description=DESCRIPTION,
@@ -35,14 +36,14 @@ setup(
python_requires=">=3.5", python_requires=">=3.5",
url=URL, url=URL,
packages=find_packages(exclude=["tests.*", "tests"]), packages=find_packages(exclude=["tests.*", "tests"]),
package_data={NAME: ["view/resources/locale/*", "view/resources/img/*", "view/resources/style/*", 'view/resources/style/*/img/*', "gems/*/resources/img/*", "gems/*/resources/locale/*", "desktop/*"]}, package_data={APP_PACKAGE: ["view/resources/locale/*", "view/resources/img/*", "view/resources/style/*", 'view/resources/style/*/img/*', "gems/*/resources/img/*", "gems/*/resources/locale/*", "desktop/*"]},
install_requires=requirements, install_requires=requirements,
test_suite="tests", test_suite="tests",
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"{name}={name}.app:main".format(name=NAME), "bearhub=bauh.app:main",
"{name}-tray={name}.app:tray".format(name=NAME), "bearhub-tray=bauh.app:tray",
"{name}-cli={name}.cli.app:main".format(name=NAME) "bearhub-cli=bauh.cli.app:main"
] ]
}, },
include_package_data=True, include_package_data=True,