2ddd3b4d0f
This Button allows users to see the current Application management queue by leveraging the new RigoDaemon API.
1885 lines
65 KiB
Python
1885 lines
65 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright (C) 2009 Canonical
|
|
Copyright (C) 2012 Fabio Erculiani
|
|
|
|
Authors:
|
|
Michael Vogt
|
|
Fabio Erculiani
|
|
|
|
This program is free software; you can redistribute it and/or modify it under
|
|
the terms of the GNU General Public License as published by the Free Software
|
|
Foundation; version 3.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
this program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
"""
|
|
import os
|
|
import time
|
|
from collections import deque
|
|
from threading import Semaphore, Lock
|
|
|
|
from gi.repository import GObject
|
|
|
|
from entropy.const import const_debug_write, const_debug_enabled, \
|
|
const_convert_to_unicode, etpConst
|
|
from entropy.exceptions import DependenciesNotRemovable, \
|
|
DependenciesNotFound, DependenciesCollision, EntropyException
|
|
from entropy.i18n import _
|
|
from entropy.misc import ParallelTask
|
|
from entropy.services.client import WebService
|
|
from entropy.client.services.interfaces import ClientWebService, \
|
|
DocumentList
|
|
|
|
import entropy.tools
|
|
import entropy.dep
|
|
|
|
from rigo.enums import Icons
|
|
from rigo.utils import build_application_store_url, escape_markup, \
|
|
prepare_markup
|
|
|
|
class ReviewStats(object):
|
|
|
|
NO_RATING = 0
|
|
|
|
def __init__(self, app):
|
|
self.app = app
|
|
self.ratings_average = None
|
|
self.downloads_total = -1
|
|
self.rating_spread = [0,0,0,0,0]
|
|
self.dampened_rating = 3.00
|
|
|
|
@property
|
|
def downloads_total_markup(self):
|
|
"""
|
|
Return a nicer representation of the total downloads amount.
|
|
"""
|
|
total = self.downloads_total
|
|
if total < 100:
|
|
text = "< 100"
|
|
elif total < 600:
|
|
text = "500+"
|
|
elif total < 1100:
|
|
text = "1.000+"
|
|
elif total < 2100:
|
|
text = "2.000+"
|
|
elif total < 5100:
|
|
text = "5.000+"
|
|
elif total < 10100:
|
|
text = "10.000+"
|
|
elif total < 20000:
|
|
text = "15.000+"
|
|
elif total < 30000:
|
|
text = "20.000+"
|
|
elif total < 60000:
|
|
text = "50.000+"
|
|
elif total < 110000:
|
|
text = "100.000+"
|
|
elif total < 210000:
|
|
text = "200.000+"
|
|
elif total < 510000:
|
|
text = "500.000+"
|
|
elif total < 1010000:
|
|
text = "1.000.000+"
|
|
elif total < 1510000:
|
|
text = "1.500.000+"
|
|
elif total < 2010000:
|
|
text = "2.000.000+"
|
|
elif total < 5010000:
|
|
text = "5.000.000+"
|
|
elif total < 10010000:
|
|
text = "10.000.000+"
|
|
elif total < 50010000:
|
|
text = "50.000.000+"
|
|
else:
|
|
text = "100.000.000+"
|
|
return escape_markup(text)
|
|
|
|
def __repr__(self):
|
|
return ("<ReviewStats '%s' ratings_average='%s' downloads_total='%s'"
|
|
" rating_spread='%s' dampened_rating='%s'>" %
|
|
(self.app, self.ratings_average, self.downloads_total,
|
|
self.rating_spread, self.dampened_rating))
|
|
|
|
class ApplicationMetadata(object):
|
|
"""
|
|
This is the Entropy metadata manager for Application objects.
|
|
These object can register their request here, asynchronously
|
|
and get the response when it's ready.
|
|
For example, they can allocate metadata requests, passing
|
|
a callback method that will be called when the data is available.
|
|
"""
|
|
|
|
@staticmethod
|
|
def start():
|
|
"""
|
|
Start asynchronous Entropy Metadata retrieveal.
|
|
"""
|
|
for th_info in ApplicationMetadata._REGISTERED_THREAD_INFO:
|
|
thread = th_info[0]
|
|
thread.start()
|
|
|
|
@staticmethod
|
|
def _rating_thread_body():
|
|
"""
|
|
Thread executing package rating remote data retrieval.
|
|
"""
|
|
request_list = ["vote", "down"]
|
|
return ApplicationMetadata._generic_thread_body(
|
|
"RatingThread", ApplicationMetadata._RATING_SEM,
|
|
ApplicationMetadata._RATING_DISCARD_SIGNALS,
|
|
ApplicationMetadata._RATING_THREAD_SLEEP_SECS,
|
|
ApplicationMetadata._RATING_QUEUE,
|
|
ApplicationMetadata._RATING_LOCK,
|
|
ApplicationMetadata._RATING_IN_FLIGHT,
|
|
request_list)
|
|
|
|
@staticmethod
|
|
def _icon_thread_body():
|
|
"""
|
|
Thread executing package icon remote data retrieval.
|
|
"""
|
|
request_list = ["icon"]
|
|
return ApplicationMetadata._generic_thread_body(
|
|
"IconThread", ApplicationMetadata._ICON_SEM,
|
|
ApplicationMetadata._ICON_DISCARD_SIGNALS,
|
|
ApplicationMetadata._ICON_THREAD_SLEEP_SECS,
|
|
ApplicationMetadata._ICON_QUEUE,
|
|
ApplicationMetadata._ICON_LOCK,
|
|
ApplicationMetadata._ICON_IN_FLIGHT,
|
|
request_list)
|
|
|
|
@staticmethod
|
|
def _generic_thread_body(name, sem, discard_signals, sleep_secs,
|
|
queue, mutex, in_flight, request_list):
|
|
"""
|
|
Thread executing generic (both rating and doc) metadata retrieval.
|
|
"""
|
|
cache_miss = WebService.CacheMiss
|
|
ws_exception = WebService.WebServiceException
|
|
|
|
def _callback_launch(key, repo_id, cb_ts_list, outcome):
|
|
for (cb, ts) in cb_ts_list:
|
|
with mutex:
|
|
in_flight.discard((key, repo_id))
|
|
if cb is not None:
|
|
outcome_values = []
|
|
for request in request_list:
|
|
outcome_values.append(
|
|
outcome[request].get((key, repo_id)))
|
|
task = ParallelTask(cb, outcome_values, ts)
|
|
task.name = "%sCb{%s, %s}" % (name, repo_id, key)
|
|
task.start()
|
|
|
|
while True:
|
|
sem.acquire()
|
|
for discard_signal in discard_signals:
|
|
discard_signal.set(False)
|
|
const_debug_write(__name__,
|
|
"%s, waking up" % (name,))
|
|
# sleep a bit in order to catch more flies
|
|
time.sleep(sleep_secs)
|
|
# now catch the flies
|
|
local_queue = []
|
|
while True:
|
|
try:
|
|
local_queue.append(queue.popleft())
|
|
except IndexError:
|
|
# no more items
|
|
break
|
|
|
|
if const_debug_enabled():
|
|
const_debug_write(__name__,
|
|
"%s, got: %s" % (name, local_queue,))
|
|
if not local_queue:
|
|
continue
|
|
|
|
# setup dispatch map
|
|
pkg_key_map = {}
|
|
# and repository map
|
|
repo_map = {}
|
|
ws_map = {}
|
|
visible_cb_map = {}
|
|
for item in local_queue:
|
|
webserv, key, repo_id, cb, still_vis_cb, ts = item
|
|
|
|
obj = pkg_key_map.setdefault((key, repo_id), [])
|
|
obj.append((cb, ts))
|
|
|
|
obj = repo_map.setdefault(repo_id, set())
|
|
obj.add(key)
|
|
ws_map[repo_id] = webserv
|
|
|
|
obj = visible_cb_map.setdefault(key, [])
|
|
obj.append(still_vis_cb)
|
|
|
|
request_outcome = {}
|
|
|
|
for repo_id, keys in repo_map.items():
|
|
|
|
webserv = ws_map[repo_id]
|
|
request_map = {
|
|
"vote": (webserv.get_votes, {}),
|
|
"down": (webserv.get_downloads, {}),
|
|
# for these two, service_cache is broken
|
|
# and actually useless.
|
|
"icon": (webserv.get_icons, {}),
|
|
"comment": (webserv.get_comments, {"latest": True,}),
|
|
}
|
|
|
|
for key in keys:
|
|
|
|
# discard signal received ?
|
|
do_discard = False
|
|
for discard_signal in discard_signals:
|
|
if discard_signal.get():
|
|
do_discard = True
|
|
break
|
|
if do_discard:
|
|
break
|
|
|
|
uncached_requests = []
|
|
outcome = {}
|
|
for request in request_list:
|
|
|
|
request_outcome = outcome.setdefault(request, {})
|
|
request_func, req_kwargs = request_map[request]
|
|
|
|
try:
|
|
request_outcome[(key, repo_id)] = request_func(
|
|
[key], cache=True, cached=True,
|
|
**req_kwargs)[key]
|
|
except cache_miss:
|
|
uncached_requests.append(request)
|
|
|
|
# checking if we're still visible
|
|
is_visible = False
|
|
for vis_cb in visible_cb_map[key]:
|
|
if vis_cb is None:
|
|
is_visible = True
|
|
break
|
|
if vis_cb():
|
|
is_visible = True
|
|
break
|
|
if not is_visible:
|
|
# don't query the remote service
|
|
uncached_requests = []
|
|
|
|
for request in uncached_requests:
|
|
|
|
request_outcome = outcome[request]
|
|
request_func, req_kwargs = request_map[request]
|
|
|
|
try:
|
|
request_outcome[(key, repo_id)] = request_func(
|
|
[key], cache = True, **req_kwargs)[key]
|
|
except ws_exception as wse:
|
|
const_debug_write(
|
|
__name__,
|
|
"%s, WebServiceExc: %s" % (name, wse,)
|
|
)
|
|
request_outcome[(key, repo_id)] = None
|
|
|
|
cb_ts_list = pkg_key_map[(key, repo_id)]
|
|
_callback_launch(key, repo_id, cb_ts_list, outcome)
|
|
|
|
# don't worry about races
|
|
discarded = False
|
|
for discard_signal in discard_signals:
|
|
if discard_signal.get():
|
|
const_debug_write(
|
|
__name__,
|
|
"%s, discard signal received." % (name,)
|
|
)
|
|
discard_signal.set(False)
|
|
request_outcome.clear()
|
|
discarded = True
|
|
break
|
|
|
|
if discarded:
|
|
continue
|
|
|
|
|
|
@staticmethod
|
|
def discard():
|
|
"""
|
|
Discard all the queued requests. No longer needed.
|
|
"""
|
|
const_debug_write(__name__,
|
|
"ApplicationMetadata.discard() called")
|
|
for th_info in ApplicationMetadata._REGISTERED_THREAD_INFO:
|
|
th, queue, sem, lock, \
|
|
discard_signals, in_flight = th_info
|
|
while True:
|
|
try:
|
|
queue.popleft()
|
|
# we could use blocking mode, but no actual need
|
|
sem.acquire(False)
|
|
except IndexError:
|
|
break
|
|
with lock:
|
|
in_flight.clear()
|
|
for discard_signal in discard_signals:
|
|
discard_signal.set(True)
|
|
|
|
@staticmethod
|
|
def _download_document(entropy_ws, document, cache=True):
|
|
"""
|
|
Dowload Document (Icon, File, Image, etc) through the Entropy
|
|
WebService interface.
|
|
Return path to just downloaded Document if success, None otherwise.
|
|
"""
|
|
# avoid bursts of downloads caused by race on get&set
|
|
# (check if file has been downloaded && download if not)
|
|
mutex = ApplicationMetadata._DOWNLOAD_DOCUMENT_LOCK
|
|
url_mutexes = ApplicationMetadata._DOWNLOAD_DOCUMENT_URL_LOCKS
|
|
url = document.document_url()
|
|
if url is None:
|
|
return None
|
|
|
|
with mutex:
|
|
lock = url_mutexes.get(url)
|
|
if lock is None:
|
|
lock = Lock()
|
|
url_mutexes[url] = lock
|
|
|
|
with lock:
|
|
def _complete():
|
|
with mutex:
|
|
# clear out url_mutexes for url
|
|
# if we get here, we have exclusive access
|
|
url_mutexes.pop(url, None)
|
|
|
|
# once here, check if file has been already downloaded
|
|
_local_path = document.local_document()
|
|
if os.path.isfile(_local_path):
|
|
_complete()
|
|
return _local_path
|
|
|
|
local_path = None
|
|
try:
|
|
local_path = entropy_ws.get_document_url(document,
|
|
cache=cache)
|
|
except ClientWebService.DocumentError as err:
|
|
const_debug_write(__name__,
|
|
"_download_document: document error: %s" % (
|
|
err,))
|
|
|
|
# the last one close the door please
|
|
_complete()
|
|
return local_path
|
|
|
|
@staticmethod
|
|
def _enqueue_rating(webservice, package_key, repository_id, callback,
|
|
_still_visible_cb):
|
|
"""
|
|
Enqueue the retrieval of the Rating for package key in given repository.
|
|
Once the data is ready, callback() will be called passing the
|
|
payload.
|
|
This method is asynchronous and returns as soon as possible.
|
|
callback() signature is: callback(payload, request_timestamp_float).
|
|
If data is not available, payload will be None.
|
|
callback argument can be None.
|
|
_RATING_LOCK must be acquired by caller.
|
|
"""
|
|
if const_debug_enabled():
|
|
const_debug_write(
|
|
__name__,
|
|
"_enqueue_rating: %s, %s" % (package_key, repository_id))
|
|
request_time = time.time()
|
|
in_flight = ApplicationMetadata._RATING_IN_FLIGHT
|
|
queue = ApplicationMetadata._RATING_QUEUE
|
|
sem = ApplicationMetadata._RATING_SEM
|
|
in_flight.add((package_key, repository_id))
|
|
queue.append((webservice, package_key,
|
|
repository_id, callback,
|
|
_still_visible_cb, request_time))
|
|
sem.release()
|
|
|
|
@staticmethod
|
|
def _enqueue_icon(webservice, package_key, repository_id, callback,
|
|
_still_visible_cb):
|
|
"""
|
|
Enqueue the retrieval of the Icon for package key in given repository.
|
|
Once the data is ready, callback() will be called passing the
|
|
payload.
|
|
This method is asynchronous and returns as soon as possible.
|
|
callback() signature is: callback(payload, request_timestamp_float).
|
|
If data is not available, payload will be None.
|
|
callback argument can be None.
|
|
_ICON_LOCK must be acquired by caller.
|
|
"""
|
|
if const_debug_enabled():
|
|
const_debug_write(
|
|
__name__,
|
|
"_enqueue_icon: %s, %s" % (package_key, repository_id))
|
|
|
|
request_time = time.time()
|
|
in_flight = ApplicationMetadata._ICON_IN_FLIGHT
|
|
queue = ApplicationMetadata._ICON_QUEUE
|
|
sem = ApplicationMetadata._ICON_SEM
|
|
in_flight.add((package_key, repository_id))
|
|
queue.append((webservice, package_key,
|
|
repository_id, callback,
|
|
_still_visible_cb, request_time))
|
|
sem.release()
|
|
|
|
@staticmethod
|
|
def lazy_get_rating(entropy_ws, package_key, repository_id,
|
|
callback=None, _still_visible_cb=None):
|
|
"""
|
|
Return the Rating (stars) for given package key, if it's available
|
|
in local cache. At the same time, if not available and not already
|
|
enqueued for download, do it, atomically.
|
|
Raise WebService.CacheMiss if not available, the rating otherwise
|
|
(tuple composed by (vote, number_of_downloads)).
|
|
"""
|
|
webserv = entropy_ws.get(repository_id)
|
|
if webserv is None:
|
|
return None
|
|
|
|
try:
|
|
vote = webserv.get_votes(
|
|
[package_key], cache=True, cached=True)[package_key]
|
|
down = webserv.get_downloads(
|
|
[package_key], cache=True, cached=True)[package_key]
|
|
except WebService.CacheMiss as exc:
|
|
if const_debug_enabled():
|
|
const_debug_write(__name__,
|
|
"lazy_get_rating: cache miss for: %s, %s" % (
|
|
package_key, repository_id))
|
|
# not in cache
|
|
flight_key = (package_key, repository_id)
|
|
with ApplicationMetadata._RATING_LOCK:
|
|
if flight_key not in ApplicationMetadata._RATING_IN_FLIGHT:
|
|
# enqueue a new rating then
|
|
ApplicationMetadata._enqueue_rating(
|
|
webserv, package_key,
|
|
repository_id, callback,
|
|
_still_visible_cb)
|
|
# let caller handle this
|
|
raise exc
|
|
|
|
return vote, down
|
|
|
|
@staticmethod
|
|
def download_rating_async(entropy_ws, package_key, repository_id,
|
|
callback):
|
|
"""
|
|
Asynchronously download updated information regarding the
|
|
Rating of given package.
|
|
This request disables local cache usage and directly queries
|
|
the remote Web Service.
|
|
Once data is available, callback will be called passing the returned
|
|
payload as argument.
|
|
For this method, the signature of callback is:
|
|
callback((vote, Number_of_downloads)) [one argument, which is a tuple]
|
|
If the Web Service is not available for repository, None is passed as
|
|
payload of callback.
|
|
Please note that the callback is called from another thread.
|
|
"""
|
|
webserv = entropy_ws.get(repository_id)
|
|
if webserv is None:
|
|
task = ParallelTask(callback, None)
|
|
task.name = "DownloadRatingAsync::None"
|
|
task.daemon = True
|
|
task.start()
|
|
return None
|
|
|
|
def _getter():
|
|
outcome = None
|
|
try:
|
|
vote = webserv.get_votes(
|
|
[package_key], cache=False)[package_key]
|
|
down = webserv.get_downloads(
|
|
[package_key], cache=False)[package_key]
|
|
outcome = (vote, down)
|
|
except WebService.WebServiceException as err:
|
|
const_debug_write(
|
|
__name__,
|
|
"Application{%s}.download_rating_async: %s" % (
|
|
package_key, err,))
|
|
finally:
|
|
# ignore exceptions, if any, and always
|
|
# call callback.
|
|
callback(outcome)
|
|
|
|
task = ParallelTask(_getter)
|
|
task.name = "DownloadRatingAsync::Getter"
|
|
task.daemon = True
|
|
task.start()
|
|
|
|
@staticmethod
|
|
def lazy_get_icon(entropy_ws, package_key, repository_id,
|
|
callback=None, _still_visible_cb=None):
|
|
"""
|
|
Return a DocumentList of Icons for given package key, if it's available
|
|
in local cache. At the same time, if not available and not already
|
|
enqueued for download, do it, atomically.
|
|
Return None if not available, or DocumentList (see Entropy Services
|
|
API) otherwise. DocumentList contains a list of Document objects,
|
|
and calling Document.local_document() would give you the image path.
|
|
If cache is not available however, WebService.CacheMiss is raised and
|
|
an asynchronous request shall be submitted to the remove web service,
|
|
once data is ready, callback will be called.
|
|
"""
|
|
webserv = entropy_ws.get(repository_id)
|
|
if webserv is None:
|
|
return None
|
|
|
|
def _pick_icon(icons):
|
|
return icons[0]
|
|
|
|
def _icon_callback(outcomes, ts):
|
|
icons = outcomes[0]
|
|
if not icons:
|
|
# sadly, no icons
|
|
return
|
|
icon = _pick_icon(icons)
|
|
local_path = icon.local_document()
|
|
if not os.path.isfile(local_path):
|
|
local_path = ApplicationMetadata._download_document(
|
|
webserv, icon)
|
|
if local_path:
|
|
# only if successful, otherwise we fall into
|
|
# infinite loop
|
|
callback(icons)
|
|
|
|
try:
|
|
icons = webserv.get_icons(
|
|
[package_key], cache=True, cached=True)[package_key]
|
|
except WebService.CacheMiss as exc:
|
|
if const_debug_enabled():
|
|
const_debug_write(__name__,
|
|
"lazy_get_icon: cache miss for: %s, %s" % (
|
|
package_key, repository_id))
|
|
# not in cache
|
|
flight_key = (package_key, repository_id)
|
|
with ApplicationMetadata._ICON_LOCK:
|
|
if flight_key not in ApplicationMetadata._ICON_IN_FLIGHT:
|
|
# enqueue a new rating then
|
|
ApplicationMetadata._enqueue_icon(
|
|
webserv, package_key,
|
|
repository_id, _icon_callback,
|
|
_still_visible_cb)
|
|
# let caller handle this
|
|
raise exc
|
|
|
|
if not icons:
|
|
return None
|
|
|
|
# pick the first icon as document icon
|
|
icon = _pick_icon(icons)
|
|
# check if we have the file on-disk, otherwise
|
|
# spawn the fetch in parallel.
|
|
icon_path = icon.local_document()
|
|
if not os.path.isfile(icon_path):
|
|
task = ParallelTask(_icon_callback, [icons], time.time())
|
|
task.daemon = True
|
|
task.name = "FetchIconCb{(%s, %s)}" % ((package_key, repository_id))
|
|
task.start()
|
|
|
|
return icon
|
|
|
|
@staticmethod
|
|
def download_icon_async(entropy_ws, package_key, repository_id,
|
|
callback):
|
|
"""
|
|
Asynchronously download updated information regarding the
|
|
Icon of given package.
|
|
This request disables local cache usage and directly queries
|
|
the remote Web Service.
|
|
Once data is available, callback will be called passing the returned
|
|
payload as argument.
|
|
For this method, the signature of callback is:
|
|
callback(Icon object)
|
|
If the Web Service is not available for repository, None is passed as
|
|
payload of callback.
|
|
Please note that the callback is called from another thread.
|
|
"""
|
|
webserv = entropy_ws.get(repository_id)
|
|
if webserv is None:
|
|
task = ParallelTask(callback, None)
|
|
task.name = "DownloadIconAsync::None"
|
|
task.daemon = True
|
|
task.start()
|
|
return None
|
|
|
|
def _pick_icon(icons):
|
|
return icons[0]
|
|
|
|
def _getter():
|
|
outcome = None
|
|
try:
|
|
icons = webserv.get_icons(
|
|
[package_key], cache=False)[package_key]
|
|
|
|
# pick the first icon as document icon
|
|
icon = _pick_icon(icons)
|
|
# check if we have the file on-disk, otherwise
|
|
# spawn the fetch in parallel.
|
|
icon_path = icon.local_document()
|
|
if not os.path.isfile(icon_path):
|
|
local_path = ApplicationMetadata._download_document(
|
|
webserv, icon)
|
|
if local_path:
|
|
outcome = icon
|
|
except WebService.WebServiceException as err:
|
|
const_debug_write(
|
|
__name__,
|
|
"Application{%s}.download_icon_async: %s" % (
|
|
package_key, err,))
|
|
finally:
|
|
# ignore exceptions, if any, and always
|
|
# call callback.
|
|
callback(outcome)
|
|
|
|
task = ParallelTask(_getter)
|
|
task.name = "DownloadIconAsync::Getter"
|
|
task.daemon = True
|
|
task.start()
|
|
|
|
@staticmethod
|
|
def download_comments_async(entropy_ws, package_key, repository_id,
|
|
offset, callback):
|
|
"""
|
|
Asynchronously download updated information regarding the
|
|
comments of the given application.
|
|
This request disables local cache usage and directly queries
|
|
the remote Web Service.
|
|
Once data is available, callback will be called passing the returned
|
|
payload as argument.
|
|
For this method, the signature of callback is:
|
|
callback(DocumentList)
|
|
If the Web Service is not available for repository, None is passed as
|
|
payload of callback.
|
|
Please note that the callback is called from another thread.
|
|
"""
|
|
webserv = entropy_ws.get(repository_id)
|
|
if webserv is None:
|
|
task = ParallelTask(callback, None)
|
|
task.name = "DownloadCommentsAsync::None"
|
|
task.daemon = True
|
|
task.start()
|
|
return None
|
|
|
|
def _getter():
|
|
outcome = None
|
|
try:
|
|
outcome = webserv.get_comments(
|
|
[package_key], cache=False,
|
|
latest=True, offset=offset)[package_key]
|
|
except WebService.WebServiceException as err:
|
|
const_debug_write(
|
|
__name__,
|
|
"Application{%s}.download_comments_async: %s" % (
|
|
package_key, err,))
|
|
finally:
|
|
# ignore exceptions, if any, and always
|
|
# call callback.
|
|
callback(outcome)
|
|
|
|
task = ParallelTask(_getter)
|
|
task.name = "DownloadCommentsAsync::Getter"
|
|
task.daemon = True
|
|
task.start()
|
|
|
|
@staticmethod
|
|
def download_images_async(entropy_ws, package_key, repository_id,
|
|
offset, callback, ignore_icons=True):
|
|
"""
|
|
Asynchronously download updated information regarding the images
|
|
of the given application.
|
|
This request disables local cache usage and directly queries
|
|
the remote Web Service.
|
|
Once data is available, callback will be called passing the returned
|
|
payload as argument.
|
|
For this method, the signature of callback is:
|
|
callback(DocumentList)
|
|
If the Web Service is not available for repository, None is passed as
|
|
payload of callback.
|
|
Please note that the callback is called from another thread.
|
|
"""
|
|
webserv = entropy_ws.get(repository_id)
|
|
if webserv is None:
|
|
task = ParallelTask(callback, None)
|
|
task.name = "DownloadImagesAsync::None"
|
|
task.daemon = True
|
|
task.start()
|
|
return None
|
|
|
|
def _getter():
|
|
outcome = None
|
|
try:
|
|
images = webserv.get_images(
|
|
[package_key], cache=False,
|
|
latest=True, offset=offset)[package_key]
|
|
|
|
fetched_images = []
|
|
for image in images:
|
|
if image.is_icon() and ignore_icons:
|
|
continue
|
|
# check if we have the file on-disk, otherwise
|
|
# spawn the fetch in parallel.
|
|
image_path = image.local_document()
|
|
if not os.path.isfile(image_path):
|
|
local_path = ApplicationMetadata._download_document(
|
|
webserv, image)
|
|
if local_path:
|
|
fetched_images.append(image)
|
|
else:
|
|
fetched_images.append(image)
|
|
|
|
# final DocumentList may contain less elements
|
|
_outcome = DocumentList(
|
|
images.package_name(),
|
|
images.has_more(),
|
|
images.offset())
|
|
_outcome.extend(fetched_images)
|
|
outcome = _outcome
|
|
|
|
except WebService.WebServiceException as err:
|
|
const_debug_write(
|
|
__name__,
|
|
"Application{%s}.download_images_async: %s" % (
|
|
package_key, err,))
|
|
finally:
|
|
# ignore exceptions, if any, and always
|
|
# call callback.
|
|
callback(outcome)
|
|
|
|
task = ParallelTask(_getter)
|
|
task.name = "DownloadImagesAsync::Getter"
|
|
task.daemon = True
|
|
task.start()
|
|
|
|
|
|
class SignalBoolean(object):
|
|
|
|
def __init__(self, val):
|
|
self.__val = val
|
|
|
|
def set(self, val):
|
|
self.__val = val
|
|
|
|
def get(self):
|
|
return self.__val
|
|
|
|
_REGISTERED_THREAD_INFO = []
|
|
|
|
_RATING_WORKERS = 3
|
|
_ICON_WORKERS = 3
|
|
|
|
_ICON_DISCARD_SIGNALS = []
|
|
_RATING_DISCARD_SIGNALS = []
|
|
|
|
_DOWNLOAD_DOCUMENT_LOCK = Lock()
|
|
_DOWNLOAD_DOCUMENT_URL_LOCKS = {}
|
|
|
|
# Application Rating logic
|
|
_RATING_QUEUE = deque()
|
|
def _rating_thread_body_wrapper():
|
|
return ApplicationMetadata._rating_thread_body()
|
|
_RATING_THREAD_SLEEP_SECS = 2.0
|
|
_RATING_SEM = Semaphore(0)
|
|
_RATING_LOCK = Lock()
|
|
_RATING_IN_FLIGHT = set()
|
|
|
|
for i in range(_RATING_WORKERS):
|
|
th = ParallelTask(_rating_thread_body_wrapper)
|
|
th.daemon = True
|
|
th.name = "RatingThread-%s" % (i,)
|
|
discard_signal = SignalBoolean(False)
|
|
_REGISTERED_THREAD_INFO.append(
|
|
(th, _RATING_QUEUE,
|
|
_RATING_SEM, _RATING_LOCK,
|
|
_RATING_DISCARD_SIGNALS,
|
|
_RATING_IN_FLIGHT))
|
|
_RATING_DISCARD_SIGNALS.append(discard_signal)
|
|
|
|
# Application Icons logic
|
|
_ICON_QUEUE = deque()
|
|
def _icon_thread_body_wrapper():
|
|
return ApplicationMetadata._icon_thread_body()
|
|
_ICON_THREAD_SLEEP_SECS = 2.0
|
|
_ICON_SEM = Semaphore(0)
|
|
_ICON_LOCK = Lock()
|
|
_ICON_IN_FLIGHT = set()
|
|
|
|
for i in range(_ICON_WORKERS):
|
|
th = ParallelTask(_icon_thread_body_wrapper)
|
|
th.daemon = True
|
|
th.name = "IconThread-%s" % (i,)
|
|
discard_signal = SignalBoolean(False)
|
|
_REGISTERED_THREAD_INFO.append(
|
|
(th, _ICON_QUEUE,
|
|
_ICON_SEM, _ICON_LOCK,
|
|
_ICON_DISCARD_SIGNALS,
|
|
_ICON_IN_FLIGHT))
|
|
_ICON_DISCARD_SIGNALS.append(discard_signal)
|
|
|
|
|
|
# this is a very lean class as its used in the main listview
|
|
# and there are a lot of application objects in memory
|
|
class Application(object):
|
|
"""
|
|
The central software item abstraction. it contains a
|
|
pkgname that is always available and a optional appname
|
|
for packages with multiple applications
|
|
There is also a __cmp__ method and a name property
|
|
"""
|
|
|
|
class AcceptLicenseError(EntropyException):
|
|
|
|
"""
|
|
Exception raised when Application can be installed
|
|
but licenses have to be accepted. The get() method
|
|
returns a mapping composed by license id as key and
|
|
list of Application objects as value.
|
|
"""
|
|
|
|
def __init__(self, license_map):
|
|
self._licenses = license_map
|
|
|
|
def get(self):
|
|
"""
|
|
Return the Licenses to accept in mapping form:
|
|
license id as key, list of Application objects as
|
|
value.
|
|
"""
|
|
return self._licenses
|
|
|
|
def __init__(self, entropy_client, entropy_ws, rigo_service,
|
|
package_match, redraw_callback=None, package_path=None,
|
|
children=None, vanished_callback=None):
|
|
self._entropy = entropy_client
|
|
self._entropy_ws = entropy_ws
|
|
self._service = rigo_service
|
|
self._pkg_match = package_match
|
|
self._pkg_id, self._repo_id = package_match
|
|
self._path = package_path
|
|
self._redraw_callback = redraw_callback
|
|
self._children = children
|
|
self._vanished_callback = vanished_callback
|
|
|
|
@property
|
|
def name(self):
|
|
"""Show user visible name"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
name = repo.retrieveName(self._pkg_id)
|
|
if name is None:
|
|
if self._vanished_callback is not None:
|
|
self._vanished_callback(self)
|
|
return _("N/A")
|
|
name = " ".join([x.capitalize() for x in \
|
|
name.replace("-"," ").split()])
|
|
return escape_markup(name)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def path(self):
|
|
"""
|
|
If this Application comes from a single package file,
|
|
return the package path.
|
|
"""
|
|
return self._path
|
|
|
|
@property
|
|
def children(self):
|
|
"""
|
|
If the Application is the parent of other Applications,
|
|
this object shall contain a list of children Application
|
|
objects. Otherwise, it returns None.
|
|
"""
|
|
return self._children
|
|
|
|
def is_installed(self):
|
|
"""
|
|
Return if Application is currently installed.
|
|
"""
|
|
app = self.get_installed()
|
|
return app is not None
|
|
|
|
def is_installed_app(self):
|
|
"""
|
|
Return whether this Application object describes an
|
|
Installed one.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
return self._is_installed_app()
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def _is_installed_app(self):
|
|
"""
|
|
Return whether this Application object describes an
|
|
Installed one.
|
|
"""
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
inst_repo = self._entropy.installed_repository()
|
|
return repo is inst_repo
|
|
|
|
def _get_installed(self):
|
|
"""
|
|
Application.get_installed() method body.
|
|
"""
|
|
inst_repo = self._entropy.installed_repository()
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
|
|
key_slot_tag = repo.retrieveKeySlotTag(self._pkg_id)
|
|
if key_slot_tag is None:
|
|
return None
|
|
|
|
key, slot, tag = key_slot_tag
|
|
matches = inst_repo.searchKeySlotTag(key, slot, tag)
|
|
# in the installed packages repository, matches
|
|
# must be of length < 2.
|
|
if len(matches) > 1:
|
|
return None
|
|
if not matches:
|
|
# not installed
|
|
return None
|
|
|
|
package_id = list(matches)[0]
|
|
if inst_repo is repo:
|
|
# return ourselves if we're already representing
|
|
# an installed App (is_removable() expects that)
|
|
return self
|
|
return Application(self._entropy, self._entropy_ws,
|
|
self._service,
|
|
(package_id, inst_repo.repository_id()),
|
|
redraw_callback=self._redraw_callback)
|
|
|
|
def _source_repository_id(self, repo):
|
|
"""
|
|
Return the actual repository name (if possible) if
|
|
Application is coming from the Installed Packages Repository.
|
|
"""
|
|
inst_repo = self._entropy.installed_repository()
|
|
repository_id = repo.repository_id()
|
|
if repo is inst_repo:
|
|
# get source repository id, because installed repo
|
|
# id won't work
|
|
_repository_id = repo.getInstalledPackageRepository(
|
|
self._pkg_id)
|
|
if _repository_id is not None:
|
|
repository_id = _repository_id
|
|
return repository_id
|
|
|
|
def get_installed(self):
|
|
"""
|
|
Return the Application object of the installed
|
|
application (rather than the available one), or
|
|
None if not installed.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
return self._get_installed()
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def is_updatable(self):
|
|
"""
|
|
Return if Application can be updated.
|
|
With "updated" we also mean "downgraded".
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
inst_repo = self._entropy.installed_repository()
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
if repo is inst_repo:
|
|
return False
|
|
pkgcmp = self._entropy.get_package_action(
|
|
(self._pkg_id, self._repo_id))
|
|
# 0 = reinstall
|
|
# 1 = new package
|
|
# 2 = update
|
|
# else = downgrade
|
|
if pkgcmp == 2:
|
|
return True
|
|
elif pkgcmp == 0:
|
|
return False
|
|
elif pkgcmp == 1:
|
|
return False
|
|
else:
|
|
return True
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def _get_removal_queue(self):
|
|
"""
|
|
Return Application removal queue.
|
|
"""
|
|
try:
|
|
return self._entropy.get_reverse_queue(
|
|
[(self._pkg_id, self._repo_id)])
|
|
except DependenciesNotRemovable:
|
|
return None
|
|
|
|
def get_removal_queue(self):
|
|
"""
|
|
Return a list of Applications that would be removed.
|
|
Please note that if the Application is not removable,
|
|
None is returned.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
queue = self._get_removal_queue()
|
|
if queue is None:
|
|
return None
|
|
remove = []
|
|
for pkg_match in queue:
|
|
app = Application(
|
|
self._entropy, self._entropy_ws,
|
|
self._service, pkg_match)
|
|
remove.append(app)
|
|
return remove
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def is_removable(self):
|
|
"""
|
|
Return if Application can be removed or it's part of
|
|
the Base System.
|
|
"""
|
|
installed = self.get_installed()
|
|
if installed is not self:
|
|
return installed.is_removable()
|
|
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
removable = self._entropy.validate_package_removal(
|
|
self._pkg_id)
|
|
if removable:
|
|
return True
|
|
|
|
try:
|
|
self._get_removal_queue()
|
|
return True
|
|
except DependenciesNotRemovable:
|
|
return False
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def _get_install_queue(self):
|
|
"""
|
|
Return Application install and removal queues.
|
|
"""
|
|
inst_repo = self._entropy.installed_repository()
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
if repo is inst_repo:
|
|
return None
|
|
|
|
pkg_match = self.get_details().pkg
|
|
masked = self._entropy.is_package_masked(pkg_match)
|
|
if masked:
|
|
return None
|
|
|
|
try:
|
|
install_queue, removal_queue = \
|
|
self._entropy.get_install_queue(
|
|
[pkg_match], False, False)
|
|
except DependenciesNotFound:
|
|
raise
|
|
except DependenciesCollision:
|
|
raise
|
|
|
|
return install_queue, removal_queue
|
|
|
|
def get_install_conflicts(self):
|
|
"""
|
|
Return a list of Application objects belonging to
|
|
Apps that would need to be removed of this Application
|
|
is installed.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
|
|
apps = []
|
|
queues = self._get_install_queue()
|
|
if queues is None:
|
|
return apps
|
|
install, removal = queues
|
|
del install
|
|
|
|
inst_repo = self._entropy.installed_repository()
|
|
inst_repo_id = inst_repo.repository_id()
|
|
for inst_pkg_id in removal:
|
|
app = Application(
|
|
self._entropy, self._entropy_ws,
|
|
self._service, (inst_pkg_id, inst_repo_id))
|
|
apps.append(app)
|
|
return apps
|
|
|
|
except DependenciesNotFound:
|
|
return None
|
|
except DependenciesCollision:
|
|
return None
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def get_install_queue(self):
|
|
"""
|
|
Return a tuple composed by a list of Applications that
|
|
would be installed and a list of Applications that would
|
|
be removed.
|
|
Please note that if the Application is not installable,
|
|
None is returned.
|
|
"""
|
|
app_install, app_remove = [], []
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
queues = self._get_install_queue()
|
|
if queues is None:
|
|
return None
|
|
install, removal = queues
|
|
|
|
for pkg_match in install:
|
|
app = Application(
|
|
self._entropy, self._entropy_ws,
|
|
self._service, pkg_match)
|
|
app_install.append(app)
|
|
|
|
inst_repo = self._entropy.installed_repository()
|
|
inst_repo_id = inst_repo.repository_id()
|
|
for inst_pkg_id in removal:
|
|
app = Application(
|
|
self._entropy, self._entropy_ws,
|
|
self._service, (inst_pkg_id, inst_repo_id))
|
|
app_remove.append(app)
|
|
|
|
return app_install, app_remove
|
|
|
|
except DependenciesNotFound as err:
|
|
const_debug_write(
|
|
__name__,
|
|
"get_install_queue: DependenciesNotFound: %s" % (err,))
|
|
return None
|
|
except DependenciesCollision as err:
|
|
const_debug_write(
|
|
__name__,
|
|
"get_install_queue: DependenciesCollision: %s" % (err,))
|
|
return None
|
|
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def accept_licenses(self, install_queue):
|
|
"""
|
|
Return a mapping representing the licenses to accept,
|
|
composed by license id as key and Application list as
|
|
value.
|
|
"""
|
|
pkg_map = dict((x.get_details().pkg, x) for x in install_queue)
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
license_map = {}
|
|
licenses = self._entropy.get_licenses_to_accept(
|
|
list(pkg_map.keys()))
|
|
if licenses:
|
|
for lic_id, pkg_matches in licenses.items():
|
|
obj = license_map.setdefault(lic_id, [])
|
|
for pkg_match in pkg_matches:
|
|
obj.append(pkg_map[pkg_match])
|
|
return license_map
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def is_installable(self):
|
|
"""
|
|
Return if Application can be installed or it's masked
|
|
or one of its dependencies are.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
queues = self._get_install_queue()
|
|
if queues is None:
|
|
return False
|
|
install, removal = queues
|
|
|
|
# check licenses
|
|
license_map = self.accept_licenses(install)
|
|
if license_map:
|
|
raise Application.AcceptLicenseError(license_map)
|
|
|
|
except DependenciesNotFound:
|
|
return False
|
|
except DependenciesCollision:
|
|
return False
|
|
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
return True
|
|
|
|
def is_available(self):
|
|
"""
|
|
Return if Application is actually available in repos,
|
|
for cache reasons?
|
|
The actual semantics of this method in softwarecenter
|
|
seems quite ambiguous to me.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return repo.isPackageIdAvailable(self._pkg_id)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def get_markup(self):
|
|
"""
|
|
Get Application markup text.
|
|
"""
|
|
name = self.name
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
if self._vanished_callback is not None:
|
|
if not repo.isPackageIdAvailable(self._pkg_id):
|
|
self._vanished_callback(self)
|
|
|
|
version = repo.retrieveVersion(self._pkg_id)
|
|
if version is None:
|
|
version = _("N/A")
|
|
tag = repo.retrieveTag(self._pkg_id)
|
|
if not tag:
|
|
tag = ""
|
|
else:
|
|
tag = "#" + tag
|
|
description = repo.retrieveDescription(self._pkg_id)
|
|
if description is None:
|
|
description = _("No description")
|
|
if len(description) > 79:
|
|
description = description[:80].strip() + "..."
|
|
text = "<b>%s</b> %s%s\n<small><i>%s</i></small>" % (
|
|
name,
|
|
escape_markup(version),
|
|
escape_markup(tag),
|
|
escape_markup(description))
|
|
return text
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def search(self, keyword):
|
|
"""
|
|
Match keyword against Application name. Return True if
|
|
the same contains keyword.
|
|
"""
|
|
name = self.name
|
|
if keyword in name:
|
|
return True
|
|
return keyword.lower() in name.lower()
|
|
|
|
def get_extended_markup(self):
|
|
"""
|
|
Get Application markup text (extended version).
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
if self._vanished_callback is not None:
|
|
if not repo.isPackageIdAvailable(self._pkg_id):
|
|
self._vanished_callback(self)
|
|
|
|
inst_repo = self._entropy.installed_repository()
|
|
strict = repo.getStrictData(self._pkg_id)
|
|
if strict is None:
|
|
return _("N/A")
|
|
key, slot, version, tag, revision, atom = strict
|
|
|
|
name = key.split("/", 1)[-1]
|
|
# make it cute
|
|
name = " ".join([x.capitalize() for x in \
|
|
name.replace("-"," ").split()])
|
|
name = escape_markup(name)
|
|
website = repo.retrieveHomepage(self._pkg_id)
|
|
if website:
|
|
name = "<a href=\"%s\">%s</a>" % (
|
|
escape_markup(website),
|
|
name,)
|
|
|
|
if not tag:
|
|
tag = ""
|
|
else:
|
|
tag = "#" + tag
|
|
|
|
revision_txt = "~%d" % (revision,)
|
|
|
|
description = repo.retrieveDescription(self._pkg_id)
|
|
if description is None:
|
|
description = _("No description")
|
|
if len(description) > 79:
|
|
description = description[:80].strip() + "..."
|
|
|
|
cdate = repo.retrieveCreationDate(self._pkg_id)
|
|
if cdate:
|
|
date = time.strftime("%B %d, %Y",
|
|
time.gmtime(float(cdate))).capitalize()
|
|
else:
|
|
date = _("N/A")
|
|
|
|
from_str = self._repo_id
|
|
installed_str = ""
|
|
if repo is inst_repo:
|
|
from_str = _("Installed")
|
|
else:
|
|
inst_app = self._get_installed()
|
|
if inst_app is not None:
|
|
ver = inst_app.get_details()._version
|
|
installed_str = "\n" + prepare_markup(
|
|
_("<b>%s</b> is installed") % (ver,))
|
|
|
|
repo_from = "%s <b>%s</b>" % (escape_markup(_("from")),
|
|
escape_markup(from_str),)
|
|
|
|
text = "<b>%s</b> %s%s%s\n<small><i>%s</i>\n%s, %s%s</small>" % (
|
|
name,
|
|
escape_markup(version),
|
|
escape_markup(tag),
|
|
escape_markup(revision_txt),
|
|
escape_markup(description),
|
|
escape_markup(date),
|
|
repo_from,
|
|
installed_str,
|
|
)
|
|
return text
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def get_info_markup(self):
|
|
"""
|
|
Get Application info markup text.
|
|
"""
|
|
app_store_url = build_application_store_url(self, "")
|
|
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
lic_url = "%s/license/" % (etpConst['packages_website_url'],)
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
|
|
if self._vanished_callback is not None:
|
|
if not repo.isPackageIdAvailable(self._pkg_id):
|
|
self._vanished_callback(self)
|
|
|
|
licenses = repo.retrieveLicense(self._pkg_id)
|
|
if licenses:
|
|
licenses_txt = "<b>%s</b>: " % (escape_markup(_("License")),)
|
|
licenses_txt += prepare_markup(", ".join(sorted([
|
|
"<a href=\"%s%s\">%s</a>" % (lic_url, x, x) \
|
|
for x in licenses.split()])))
|
|
else:
|
|
licenses_txt = ""
|
|
|
|
required_space = repo.retrieveOnDiskSize(self._pkg_id)
|
|
if required_space is None:
|
|
required_space = 0
|
|
required_space_txt = "<b>%s</b>: %s" % (
|
|
escape_markup(_("Required space")),
|
|
escape_markup(entropy.tools.bytes_into_human(required_space)),)
|
|
|
|
down_size = repo.retrieveSize(self._pkg_id)
|
|
if down_size is None:
|
|
down_size = 0
|
|
down_size_txt = "<b>%s</b>: %s" % (
|
|
escape_markup(_("Download size")),
|
|
escape_markup(entropy.tools.bytes_into_human(down_size)),)
|
|
|
|
digest = repo.retrieveDigest(self._pkg_id)
|
|
if digest is None:
|
|
digest = _("N/A")
|
|
digest_txt = "<b>%s</b>: %s" % (
|
|
escape_markup(_("Checksum")),
|
|
escape_markup(digest))
|
|
|
|
uses = repo.retrieveUseflags(self._pkg_id)
|
|
uses = sorted(uses)
|
|
use_list = []
|
|
use_url = "%s/useflag/" % (etpConst['packages_website_url'],)
|
|
for use in uses:
|
|
use_m = escape_markup(use)
|
|
txt = "<a href=\"%s%s\">%s</a>" % (use_url, use_m, use_m)
|
|
use_list.append(txt)
|
|
use_txt = "<b>%s</b>: " % (escape_markup(_("USE flags")),)
|
|
if use_list:
|
|
use_txt += " ".join(use_list)
|
|
else:
|
|
use_txt += escape_markup(_("No use flags"))
|
|
|
|
rdepend_id = etpConst['dependency_type_ids']['rdepend_id']
|
|
pdepend_id = etpConst['dependency_type_ids']['pdepend_id']
|
|
bdepend_id = etpConst['dependency_type_ids']['bdepend_id']
|
|
mdepend_id = etpConst['dependency_type_ids']['mdepend_id']
|
|
|
|
deps = repo.retrieveDependencies(
|
|
self._pkg_id, extended=True,
|
|
resolve_conditional_deps=False)
|
|
runtime_deps = [x for x, y in deps if y == rdepend_id]
|
|
build_deps = [x for x, y in deps if y == bdepend_id]
|
|
post_deps = [x for x, y in deps if y == pdepend_id]
|
|
manual_deps = [x for x, y in deps if y == mdepend_id]
|
|
|
|
depsorter = lambda x: entropy.dep.dep_getcpv(x)
|
|
|
|
dep_couples = [
|
|
(runtime_deps, _("Runtime dependencies")),
|
|
(build_deps, _("Build dependencies")),
|
|
(post_deps, _("Post dependencies")),
|
|
(manual_deps, _("Staff dependencies"))
|
|
]
|
|
dep_txts = []
|
|
for dep_list, dep_title in dep_couples:
|
|
if not dep_list:
|
|
continue
|
|
dep_txt = "<b>%s</b>" % (escape_markup(dep_title),)
|
|
for dep in sorted(dep_list, key=depsorter):
|
|
dep_mk = escape_markup(dep)
|
|
dep_txt += "\n <a href=\"app://%s\">%s</a>" % (
|
|
dep_mk, dep_mk,)
|
|
dep_txts.append(dep_txt)
|
|
|
|
deps_txt = "\n".join(dep_txts)
|
|
if deps_txt:
|
|
deps_txt += "\n\n"
|
|
|
|
if self._is_installed_app():
|
|
more_txt = ""
|
|
else:
|
|
more_txt = "<a href=\"%s\"><b>%s</b></a>" % (
|
|
prepare_markup(app_store_url),
|
|
escape_markup(_("Click here for more details")),)
|
|
|
|
text = "<small>%s\n%s\n%s\n%s\n%s\n%s%s</small>" % (
|
|
down_size_txt,
|
|
required_space_txt,
|
|
licenses_txt,
|
|
digest_txt,
|
|
use_txt,
|
|
deps_txt,
|
|
more_txt,
|
|
)
|
|
return text
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def get_review_stats(self, _still_visible_cb=None):
|
|
"""
|
|
Return ReviewStats object containing user review
|
|
information about this Application, like
|
|
votes and number of downloads.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
stat = ReviewStats(self)
|
|
stat.ratings_average = ReviewStats.NO_RATING
|
|
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
key_slot = repo.retrieveKeySlot(self._pkg_id)
|
|
if key_slot is None:
|
|
return stat # empty stats
|
|
key, slot = key_slot
|
|
try:
|
|
rating = ApplicationMetadata.lazy_get_rating(
|
|
self._entropy_ws, key,
|
|
self._source_repository_id(repo),
|
|
callback=self._redraw_callback,
|
|
_still_visible_cb=_still_visible_cb)
|
|
except WebService.CacheMiss:
|
|
# not in cache, return empty stats
|
|
return stat
|
|
|
|
if rating is None:
|
|
# not ready yet, return empty ratings
|
|
return stat # empty stats
|
|
vote, down = rating
|
|
if vote is not None:
|
|
stat.ratings_average = vote
|
|
if down is not None:
|
|
# otherwise 0 is shown
|
|
stat.downloads_total = down
|
|
return stat
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def get_icon(self, _still_visible_cb=None):
|
|
"""
|
|
Return Application Icon image Entropy Document object.
|
|
In case of missing icon, None is returned.
|
|
The actual outcome of this property is a tuple, composed
|
|
by the Document object (or None) and cache hit information
|
|
(True if got from local cache, False if not in local cache)
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
key_slot = repo.retrieveKeySlot(self._pkg_id)
|
|
if key_slot is None:
|
|
return None, False
|
|
|
|
key, slot = key_slot
|
|
cache_hit = True
|
|
try:
|
|
icon = ApplicationMetadata.lazy_get_icon(
|
|
self._entropy_ws, key,
|
|
self._source_repository_id(repo),
|
|
callback=self._redraw_callback,
|
|
_still_visible_cb=_still_visible_cb)
|
|
except WebService.CacheMiss:
|
|
cache_hit = False
|
|
icon = None
|
|
|
|
if const_debug_enabled():
|
|
const_debug_write(__name__,
|
|
"Application{%s}.get_icon: icon: %s, cache hit: %s" % (
|
|
self._pkg_match,
|
|
icon, cache_hit))
|
|
|
|
return icon, cache_hit
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def download_comments(self, callback, offset=0):
|
|
"""
|
|
Return Application Comments Entropy Document object.
|
|
In case of missing comments (locally), None is returned.
|
|
The actual outcome of this method is a DocumentList object.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
key_slot = repo.retrieveKeySlot(self._pkg_id)
|
|
if key_slot is None:
|
|
task = ParallelTask(callback, None)
|
|
task.name = "DownloadCommentsNoneCallback"
|
|
task.daemon = True
|
|
task.start()
|
|
return
|
|
|
|
key, slot = key_slot
|
|
ApplicationMetadata.download_comments_async(
|
|
self._entropy_ws, key, self._source_repository_id(repo),
|
|
offset, callback)
|
|
|
|
if const_debug_enabled():
|
|
const_debug_write(__name__,
|
|
"Application{%s}.download_comments called" % (
|
|
self._pkg_match,))
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def download_images(self, callback, offset=0):
|
|
"""
|
|
Return Application Images Entropy Document object.
|
|
In case of missing comments (locally), None is returned.
|
|
The actual outcome of this method is a DocumentList object.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
key_slot = repo.retrieveKeySlot(self._pkg_id)
|
|
if key_slot is None:
|
|
task = ParallelTask(callback, None)
|
|
task.name = "DownloadImagesNoneCallback"
|
|
task.daemon = True
|
|
task.start()
|
|
return
|
|
|
|
key, slot = key_slot
|
|
ApplicationMetadata.download_images_async(
|
|
self._entropy_ws, key, self._source_repository_id(repo),
|
|
offset, callback)
|
|
|
|
if const_debug_enabled():
|
|
const_debug_write(__name__,
|
|
"Application{%s}.download_images called" % (
|
|
self._pkg_match,))
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def is_webservice_available(self):
|
|
"""
|
|
Return whether the Entropy Web Service is available
|
|
for this Application.
|
|
"""
|
|
webserv = self._entropy_ws.get(self._repo_id)
|
|
if webserv:
|
|
return True
|
|
return False
|
|
|
|
def get_webservice_username(self):
|
|
"""
|
|
Return the currently logged Entropy Web Services
|
|
username, or None, if any.
|
|
"""
|
|
webserv = self._entropy_ws.get(self._repo_id)
|
|
if webserv is None:
|
|
return None
|
|
return webserv.get_credentials()
|
|
|
|
def get_transaction_progress(self):
|
|
"""
|
|
Retrieve Application transaction process from RigoDaemon.
|
|
Return -1 for no activity, and int from 0 to 100 for transaction
|
|
progress.
|
|
"""
|
|
app, state, progress = self._service.get_transaction_state()
|
|
if app is None:
|
|
return -1
|
|
if app.get_details().pkg != self.get_details().pkg:
|
|
return -1
|
|
return progress
|
|
|
|
# get a AppDetails object for this Applications
|
|
def get_details(self):
|
|
"""
|
|
Return a new AppDetails object for this application
|
|
"""
|
|
return AppDetails(self._entropy, self._entropy_ws,
|
|
self._service, self._pkg_match, self,
|
|
redraw_callback=self._redraw_callback)
|
|
|
|
def __str__(self):
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
atom = repo.retrieveAtom(self._pkg_id)
|
|
return "(%s: %s)" % (self._pkg_match, atom)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
|
|
# the details
|
|
class AppDetails(object):
|
|
"""
|
|
The details for a Application. This contains all the information
|
|
we have available like website etc
|
|
"""
|
|
|
|
def __init__(self, entropy_client, entropy_ws, rigo_service,
|
|
package_match, app, redraw_callback=None):
|
|
"""
|
|
Create a new AppDetails object.
|
|
"""
|
|
self._entropy = entropy_client
|
|
self._entropy_ws = entropy_ws
|
|
self._service = rigo_service
|
|
self._pkg_match = package_match
|
|
self._pkg_id, self._repo_id = package_match
|
|
self._app = app
|
|
self._redraw_callback = redraw_callback
|
|
|
|
@property
|
|
def channelname(self):
|
|
"""
|
|
Return Application Channel (repository identifier).
|
|
"""
|
|
return self._repo_id
|
|
|
|
@property
|
|
def sourcechannel(self):
|
|
"""
|
|
Return Application Source Channel (if this is an Installed
|
|
Application, the Channel of the original repository shall
|
|
be returned).
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return self._app._source_repository_id(repo)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def description(self):
|
|
"""
|
|
Return Application short description.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return repo.retrieveDescription(self._pkg_id)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def error(self):
|
|
return _("Application not found")
|
|
|
|
@property
|
|
def installation_date(self):
|
|
"""
|
|
Return human readable representation of the installation
|
|
date, if installed, or None otherwise.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
inst_repo = self._entropy.installed_repository()
|
|
if repo is inst_repo:
|
|
return entropy.tools.convert_unix_time_to_human_time(
|
|
float(repo.retrieveCreationDate(self._pkg_id)))
|
|
keyslot = repo.retrieveKeySlotAggregated(self._pkg_id)
|
|
pkg_id, rc = inst_repo.atomMatch(keyslot)
|
|
if pkg_id != -1:
|
|
return entropy.tools.convert_unix_time_to_human_time(
|
|
float(inst_repo.retrieveCreationDate(pkg_id)))
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def date(self):
|
|
"""
|
|
Return human readable representation of the date the
|
|
Application has been last updated.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return entropy.tools.convert_unix_time_to_human_time(
|
|
float(repo.retrieveCreationDate(self._pkg_id)))
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def licenses(self):
|
|
"""
|
|
Return list of license identifiers for Application.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
licenses = repo.retrieveLicense(self._pkg_id)
|
|
if not licenses:
|
|
return []
|
|
return licenses.split()
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def downsize(self):
|
|
"""
|
|
Return the download size in bytes.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return repo.retrieveSize(self._pkg_id)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def name(self):
|
|
"""
|
|
Return the name of the application, this will always
|
|
return Application.name. Most UI will want to use
|
|
the property display_name instead
|
|
"""
|
|
return self._app.name
|
|
|
|
@property
|
|
def display_name(self):
|
|
"""
|
|
Return the application name as it should be displayed in the UI
|
|
If the appname is defined, just return it, else return
|
|
the summary (per the spec)
|
|
"""
|
|
return self.name
|
|
|
|
@property
|
|
def pkg(self):
|
|
"""
|
|
Return unique identifier belonging to this Application.
|
|
"""
|
|
return self._pkg_match
|
|
|
|
@property
|
|
def pkgname(self):
|
|
"""
|
|
Return unmangled package name belonging to this Application.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return repo.retrieveName(self._pkg_id)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def fullname(self):
|
|
"""
|
|
Return unmangled package name belonging to this Application.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return repo.retrieveAtom(self._pkg_id)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def pkgkey(self):
|
|
"""
|
|
Return unmangled package key name belonging to this Application.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
key, slot = repo.retrieveKeySlot(self._pkg_id)
|
|
return key
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def signing_key_id(self):
|
|
"""
|
|
Return GPG key identifier used to sign the Application.
|
|
"""
|
|
return self._repo_id
|
|
|
|
@property
|
|
def version(self):
|
|
"""
|
|
Return Application version (without revision and tag).
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
return self._version
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
@property
|
|
def _version(self):
|
|
"""
|
|
Return Application version (without revision and tag).
|
|
"""
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
ver = repo.retrieveVersion(self._pkg_id)
|
|
tag = repo.retrieveTag(self._pkg_id)
|
|
if tag:
|
|
ver += etpConst['entropytagprefix'] + tag
|
|
return ver
|
|
|
|
@property
|
|
def website(self):
|
|
"""
|
|
Return Application official Website URL or None.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repo = self._entropy.open_repository(self._repo_id)
|
|
return repo.retrieveHomepage(self._pkg_id)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|