gem selector sketch

This commit is contained in:
Vinicius Moreira
2019-09-11 16:00:02 -03:00
parent 696b6169ac
commit ba5e1da11b
79 changed files with 4117 additions and 112 deletions

View File

@@ -0,0 +1,2 @@
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -0,0 +1 @@
SNAP_API_URL = 'https://search.apps.ubuntu.com/api/v1'

View File

@@ -0,0 +1,145 @@
from datetime import datetime
from typing import List, Set, Type
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion
from bauh.commons.system import SystemProcess, ProcessHandler
from bauh.gems.snap import snap, suggestions
from bauh.gems.snap.model import SnapApplication
from bauh.gems.snap.worker import SnapAsyncDataLoader
class SnapManager(SoftwareManager):
def __init__(self, context: ApplicationContext):
super(SnapManager, self).__init__(context=context)
self.i18n = context.i18n
self.api_cache = context.cache_factory.new()
context.disk_loader_factory.map(SnapApplication, self.api_cache)
def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> SnapApplication:
app = SnapApplication(publisher=app_json.get('publisher'),
rev=app_json.get('rev'),
notes=app_json.get('notes'),
app_type=app_json.get('type'),
id=app_json.get('name'),
name=app_json.get('name'),
version=app_json.get('version'),
latest_version=app_json.get('version'),
description=app_json.get('description'))
if app.publisher:
app.publisher = app.publisher.replace('*', '')
app.installed = installed
api_data = self.api_cache.get(app_json['name'])
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
if (not api_data or expired_data) and app.is_application():
if disk_loader and app.installed:
disk_loader.fill(app)
SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
else:
app.fill_cached_data(api_data)
return app
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
installed = self.read_installed(disk_loader).installed
res = SearchResult([], [], 0)
for app_json in snap.search(words):
already_installed = None
if installed:
already_installed = [i for i in installed if i.id == app_json.get('name')]
already_installed = already_installed[0] if already_installed else None
if already_installed:
res.installed.append(already_installed)
else:
res.new.append(self.map_json(app_json, installed=False, disk_loader=disk_loader))
res.total = len(res.installed) + len(res.new)
return res
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None) -> SearchResult:
installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader) for app_json in snap.read_installed()]
return SearchResult(installed, None, len(installed))
def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.downgrade_and_stream(pkg.name, root_password), wrong_error_phrase=None))
def update(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> SystemProcess:
raise Exception("'update' is not supported by {}".format(pkg.__class__.__name__))
def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.uninstall_and_stream(pkg.name, root_password)))
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
return {SnapApplication}
def clean_cache_for(self, pkg: SnapApplication):
super(SnapManager, self).clean_cache_for(pkg)
self.api_cache.delete(pkg.id)
def get_info(self, pkg: SnapApplication) -> dict:
info = snap.get_info(pkg.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed'))
info['description'] = pkg.description
info['publisher'] = pkg.publisher
info['revision'] = pkg.rev
info['name'] = pkg.name
if info.get('commands'):
info['commands'] = ' '.join(info['commands'])
return info
def get_history(self, pkg: SnapApplication) -> PackageHistory:
raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__))
def install(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.install_and_stream(pkg.name, pkg.confinement, root_password)))
def is_enabled(self) -> bool:
return snap.is_installed()
def requires_root(self, action: str, pkg: SnapApplication):
return action != 'search'
def refresh(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password)))
def prepare(self):
pass
def list_updates(self) -> List[PackageUpdate]:
pass
def list_warnings(self) -> List[str]:
if snap.get_snapd_version() == 'unavailable':
return [self.i18n['snap.notification.snapd_unavailable']]
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
res = []
sugs = [(i, p) for i, p in suggestions.ALL.items()]
sugs.sort(key=lambda t: t[1].value, reverse=True)
for sug in sugs:
if limit <= 0 or len(res) < limit:
found = snap.search(sug[0], exact_name=True)
if found:
res.append(PackageSuggestion(self.map_json(found[0], installed=False, disk_loader=None), sug[1]))
else:
break
return res

88
bauh/gems/snap/model.py Normal file
View File

@@ -0,0 +1,88 @@
from typing import List
from bauh.api.abstract.model import SoftwarePackage, PackageAction
from bauh.commons import resource
from bauh.gems.snap import ROOT_DIR
EXTRA_INSTALLED_ACTIONS = [
PackageAction(i18n_status_key='snap.action.refresh.status',
i18_label_key='snap.action.refresh.label',
icon_path=resource.get_path('img/refresh.svg', ROOT_DIR),
manager_method='refresh',
requires_root=True)
]
class SnapApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None,
description: str = None, publisher: str = None, rev: str = None, notes: str = None,
app_type: str = None, confinement: str = None):
super(SnapApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description)
self.publisher = publisher
self.rev = rev
self.notes = notes
self.type = app_type
self.confinement = confinement
def has_history(self):
return False
def has_info(self):
return True
def can_be_downgraded(self):
return self.installed
def get_type(self):
return 'snap'
def get_default_icon_path(self):
return resource.get_path('img/snap.png', ROOT_DIR)
def get_type_icon_path(self):
return self.get_default_icon_path()
def is_application(self):
return not self.type and (self.type not in ('core', 'base', 'snapd') and not self._name_starts_with(('gtk-', 'gnome-', 'kde-', 'gtk2-')))
def _name_starts_with(self, words: tuple):
for word in words:
if self.name.startswith(word):
return True
return False
def get_disk_cache_path(self):
return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name
def get_data_to_cache(self):
return {
"icon_url": self.icon_url,
'confinement': self.confinement,
'description': self.description
}
def fill_cached_data(self, data: dict):
if data:
for base_attr in ('icon_url', 'description'):
if data.get(base_attr):
setattr(self, base_attr, data[base_attr])
if data.get('confinement'):
self.confinement = data['confinement']
def get_command(self) -> str:
return "snap run " + self.name
def can_be_run(self) -> bool:
return self.installed and self.is_application()
def get_publisher(self):
return self.publisher
def get_custom_supported_actions(self) -> List[PackageAction]:
if self.installed:
return EXTRA_INSTALLED_ACTIONS

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="48"
height="48"
viewBox="0 0 47.999999 47.999999"
xml:space="preserve"
sodipodi:docname="refresh_2.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata45"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs43"><linearGradient
inkscape:collect="always"
id="linearGradient540"><stop
style="stop-color:#0088aa;stop-opacity:1;"
offset="0"
id="stop536" /><stop
style="stop-color:#0088aa;stop-opacity:0;"
offset="1"
id="stop538" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient540"
id="linearGradient542"
x1="-130.74721"
y1="-214.80638"
x2="-556.49438"
y2="110.01643"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview41"
showgrid="false"
showborder="false"
inkscape:zoom="1.9374833"
inkscape:cx="165.58246"
inkscape:cy="-10.288149"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="g8"
transform="matrix(0.10547014,0,0,0.0985161,-1.6940777,0)"
style="fill:url(#linearGradient542);fill-opacity:1">
<g
id="g6"
style="fill:url(#linearGradient542);fill-opacity:1">
<path
d="m 55.323,203.641 c 15.664,0 29.813,-9.405 35.872,-23.854 25.017,-59.604 83.842,-101.61 152.42,-101.61 37.797,0 72.449,12.955 100.23,34.442 l -21.775,3.371 c -7.438,1.153 -13.224,7.054 -14.232,14.512 -1.01,7.454 3.008,14.686 9.867,17.768 l 119.746,53.872 c 5.249,2.357 11.33,1.904 16.168,-1.205 4.83,-3.114 7.764,-8.458 7.796,-14.208 l 0.621,-131.943 c 0.042,-7.506 -4.851,-14.144 -12.024,-16.332 -7.185,-2.188 -14.947,0.589 -19.104,6.837 L 414.403,70.096 C 370.398,26.778 310.1,0 243.615,0 142.806,0 56.133,61.562 19.167,149.06 c -5.134,12.128 -3.84,26.015 3.429,36.987 7.269,10.976 19.556,17.594 32.727,17.594 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:url(#linearGradient542);fill-opacity:1" />
<path
d="m 464.635,301.184 c -7.27,-10.977 -19.558,-17.594 -32.728,-17.594 -15.664,0 -29.813,9.405 -35.872,23.854 -25.018,59.604 -83.843,101.61 -152.42,101.61 -37.798,0 -72.45,-12.955 -100.232,-34.442 l 21.776,-3.369 c 7.437,-1.153 13.223,-7.055 14.233,-14.514 1.009,-7.453 -3.008,-14.686 -9.867,-17.768 L 49.779,285.089 c -5.25,-2.356 -11.33,-1.905 -16.169,1.205 -4.829,3.114 -7.764,8.458 -7.795,14.207 l -0.622,131.943 c -0.042,7.506 4.85,14.144 12.024,16.332 7.185,2.188 14.948,-0.59 19.104,-6.839 l 16.505,-24.805 c 44.004,43.32 104.303,70.098 170.788,70.098 100.811,0 187.481,-61.561 224.446,-149.059 5.137,-12.128 3.843,-26.014 -3.425,-36.987 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:url(#linearGradient542);fill-opacity:1" />
</g>
</g>
<g
id="g10"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g12"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g14"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g16"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g18"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g20"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g22"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g24"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g26"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g28"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g30"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g32"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g34"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g36"
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g38"
transform="translate(-16.062155,-439.23)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
snap.notification.snapd_unavailable=snapd seems not to be enabled. snap packages will not be available.
snap.action.refresh.status=Refreshing
snap.action.refresh.label=Refresh

View File

@@ -0,0 +1,12 @@
snap.info.commands=comandos
snap.info.contact=contacto
snap.info.description=descripción
snap.info.license=licencia
snap.info.license.unset=no está definida
snap.info.revision=revisión
snap.info.tracking=tracking
snap.info.installed=instalado
snap.info.publisher=publicador
snap.notification.snapd_unavailable=snapd no parece estar habilitado. los paquetes snap estarán indisponibles.
snap.action.refresh.status=Actualizando
snap.action.refresh.label=Actualizar

View File

@@ -0,0 +1,12 @@
snap.info.commands=comandos
snap.info.contact=contato
snap.info.description=descrição
snap.info.license=licença
snap.info.license.unset=não definida
snap.info.revision=revisão
snap.info.tracking=tracking
snap.info.installed=instalado
snap.info.publisher=publicador
snap.notification.snapd_unavailable=snapd não parece estar habilitado. os pacotes snap estarão indisponíveis.
snap.action.refresh.status=Atualizando
snap.action.refresh.label=Atualizar

141
bauh/gems/snap/snap.py Normal file
View File

@@ -0,0 +1,141 @@
import re
import subprocess
from typing import List
from bauh.commons.system import new_root_subprocess, run_cmd
BASE_CMD = 'snap'
def is_installed():
version = get_snapd_version()
return False if version is None else True
def get_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
return res.split('\n')[0].split(' ')[-1].strip() if res else None
def get_snapd_version():
res = run_cmd('{} --version'.format(BASE_CMD), print_error=False)
if not res:
return None
else:
lines = res.split('\n')
if lines and len(lines) >= 2:
version = lines[1].split(' ')[-1].strip()
return version if version and version.lower() != 'unavailable' else None
else:
return None
def app_str_to_json(app: str) -> dict:
app_data = [word for word in app.split(' ') if word]
app_json = {
'name': app_data[0],
'version': app_data[1],
'rev': app_data[2],
'tracking': app_data[3],
'publisher': app_data[4] if len(app_data) >= 5 else None,
'notes': app_data[5] if len(app_data) >= 6 else None
}
app_json.update(get_info(app_json['name'], ('summary', 'type', 'description')))
return app_json
def get_info(app_name: str, attrs: tuple = None):
full_info_lines = run_cmd('{} info {}'.format(BASE_CMD, app_name))
data = {}
if full_info_lines:
re_attrs = r'\w+' if not attrs else '|'.join(attrs)
info_map = re.findall(r'({}):\s+(.+)'.format(re_attrs), full_info_lines)
for info in info_map:
data[info[0]] = info[1].strip()
if not attrs or 'description' in attrs:
desc = re.findall(r'\|\n+((\s+.+\n+)+)', full_info_lines)
data['description'] = ''.join([w.strip() for w in desc[0][0].strip().split('\n')]).replace('.', '.\n') if desc else None
if not attrs or 'commands' in attrs:
commands = re.findall(r'commands:\s*\n*((\s+-\s.+\s*\n)+)', full_info_lines)
data['commands'] = commands[0][0].replace('-', '').strip().split('\n') if commands else None
return data
def read_installed() -> List[dict]:
res = run_cmd('{} list'.format(BASE_CMD), print_error=False)
apps = []
if res and len(res) > 0:
lines = res.split('\n')
if not lines[0].startswith('error'):
for idx, app_str in enumerate(lines):
if idx > 0 and app_str:
apps.append(app_str_to_json(app_str))
return apps
def search(word: str, exact_name: bool = False) -> List[dict]:
apps = []
res = run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False)
if res:
res = res.split('\n')
if not res[0].startswith('No matching'):
for idx, app_str in enumerate(res):
if idx > 0 and app_str:
app_data = [word for word in app_str.split(' ') if word]
if exact_name and app_data[0] != word:
continue
apps.append({
'name': app_data[0],
'version': app_data[1],
'publisher': app_data[2],
'notes': app_data[3] if app_data[3] != '-' else None,
'summary': app_data[4] if len(app_data) == 5 else '',
'rev': None,
'tracking': None,
'type': None
})
if exact_name and len(apps) > 0:
break
return apps
def uninstall_and_stream(app_name: str, root_password: str):
return new_root_subprocess([BASE_CMD, 'remove', app_name], root_password)
def install_and_stream(app_name: str, confinement: str, root_password: str) -> subprocess.Popen:
install_cmd = [BASE_CMD, 'install', app_name] # default
if confinement == 'classic':
install_cmd.append('--classic')
return new_root_subprocess(install_cmd, root_password)
def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'revert', app_name], root_password)
def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'refresh', app_name], root_password)

View File

@@ -0,0 +1,13 @@
from bauh.api.abstract.model import SuggestionPriority
ALL = {
'whatsdesk': SuggestionPriority.TOP,
'slack': SuggestionPriority.HIGH,
'discord': SuggestionPriority.HIGH,
'yakyak': SuggestionPriority.MEDIUM,
'instagraph': SuggestionPriority.MEDIUM,
'gimp': SuggestionPriority.MEDIUM,
'pycharm-professional': SuggestionPriority.LOW,
'eclipse': SuggestionPriority.LOW,
'supertuxkart': SuggestionPriority.LOW
}

71
bauh/gems/snap/worker.py Normal file
View File

@@ -0,0 +1,71 @@
import traceback
from threading import Thread
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import PackageStatus
from bauh.gems.snap import snap
from bauh.gems.snap.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication
class SnapAsyncDataLoader(Thread):
def __init__(self, app: SnapApplication, manager: SoftwareManager, api_cache: MemoryCache,
context: ApplicationContext):
super(SnapAsyncDataLoader, self).__init__(daemon=True)
self.app = app
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
self.manager = manager
self.http_client = context.http_client
self.api_cache = api_cache
self.persist = False
self.download_icons = context.download_icons
self.logger = context.logger
def run(self):
if self.app:
self.app.status = PackageStatus.LOADING_DATA
try:
res = self.http_client.session.get('{}/search?q={}'.format(SNAP_API_URL, self.app.name))
if res:
try:
snap_list = res.json()['_embedded']['clickindex:package']
except:
self.logger.warning('Snap API response responded differently from expected for app: {}'.format(self.app.name))
return
if not snap_list:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
else:
snap_data = snap_list[0]
api_data = {
'confinement': snap_data.get('confinement'),
'description': snap_data.get('description'),
'icon_url': snap_data.get('icon_url') if self.download_icons else None
}
self.api_cache.add(self.app.id, api_data)
self.app.confinement = api_data['confinement']
self.app.icon_url = api_data['icon_url']
if not api_data.get('description'):
api_data['description'] = snap.get_info(self.app.name, ('description',)).get('description')
self.app.description = api_data['description']
self.persist = self.app.supports_disk_cache()
else:
self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode()))
except:
self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id))
traceback.print_exc()
self.app.status = PackageStatus.READY
if self.persist:
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)