mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Change app catalog API to support categories
This commit is contained in:
parent
3951f5e1b8
commit
06fe3504b3
3 changed files with 70 additions and 35 deletions
|
@ -551,7 +551,10 @@ app:
|
||||||
full: --full
|
full: --full
|
||||||
help: Display all details, including the app manifest and various other infos
|
help: Display all details, including the app manifest and various other infos
|
||||||
action: store_true
|
action: store_true
|
||||||
|
-c:
|
||||||
|
full: --with-categories
|
||||||
|
help: Also return a list of app categories
|
||||||
|
action: store_true
|
||||||
|
|
||||||
### app_list()
|
### app_list()
|
||||||
list:
|
list:
|
||||||
|
|
|
@ -57,7 +57,7 @@ APP_TMP_FOLDER = INSTALL_TMP + '/from_file'
|
||||||
APPS_CATALOG_CACHE = '/var/cache/yunohost/repo'
|
APPS_CATALOG_CACHE = '/var/cache/yunohost/repo'
|
||||||
APPS_CATALOG_CONF = '/etc/yunohost/apps_catalog.yml'
|
APPS_CATALOG_CONF = '/etc/yunohost/apps_catalog.yml'
|
||||||
APPS_CATALOG_CRON_PATH = "/etc/cron.daily/yunohost-fetch-apps-catalog"
|
APPS_CATALOG_CRON_PATH = "/etc/cron.daily/yunohost-fetch-apps-catalog"
|
||||||
APPS_CATALOG_API_VERSION = 1
|
APPS_CATALOG_API_VERSION = 2
|
||||||
APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default"
|
APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default"
|
||||||
|
|
||||||
re_github_repo = re.compile(
|
re_github_repo = re.compile(
|
||||||
|
@ -71,7 +71,7 @@ re_app_instance_name = re.compile(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def app_catalog(full=False, all=False):
|
def app_catalog(full=False, with_categories=False):
|
||||||
"""
|
"""
|
||||||
Return a dict of apps available to installation from Yunohost's app catalog
|
Return a dict of apps available to installation from Yunohost's app catalog
|
||||||
"""
|
"""
|
||||||
|
@ -80,16 +80,32 @@ def app_catalog(full=False, all=False):
|
||||||
catalog = _load_apps_catalog()
|
catalog = _load_apps_catalog()
|
||||||
installed_apps = set(_installed_apps())
|
installed_apps = set(_installed_apps())
|
||||||
|
|
||||||
for app, infos in catalog.items():
|
# Trim info for apps if not using --full
|
||||||
|
for app, infos in catalog["apps"].items():
|
||||||
infos["installed"] = app in installed_apps
|
infos["installed"] = app in installed_apps
|
||||||
|
|
||||||
|
infos["manifest"]["description"] = _value_for_locale(infos['manifest']['description'])
|
||||||
|
|
||||||
if not full:
|
if not full:
|
||||||
catalog[app] = {
|
catalog["apps"][app] = {
|
||||||
"description": _value_for_locale(infos['manifest']['description']),
|
"description": infos['manifest']['description'],
|
||||||
"level": infos["level"],
|
"level": infos["level"],
|
||||||
}
|
}
|
||||||
|
|
||||||
return {"apps": catalog}
|
# Trim info for categories if not using --full
|
||||||
|
for category in catalog["categories"]:
|
||||||
|
category["title"] = _value_for_locale(category["title"])
|
||||||
|
category["description"] = _value_for_locale(category["description"])
|
||||||
|
|
||||||
|
if not full:
|
||||||
|
catalog["categories"] = [{"id": c["id"],
|
||||||
|
"description": c["description"]}
|
||||||
|
for c in catalog["categories"]]
|
||||||
|
|
||||||
|
if not with_categories:
|
||||||
|
return {"apps": catalog["apps"]}
|
||||||
|
else:
|
||||||
|
return {"apps": catalog["apps"], "categories": catalog["categories"]}
|
||||||
|
|
||||||
|
|
||||||
def app_list(full=False):
|
def app_list(full=False):
|
||||||
|
@ -107,12 +123,7 @@ def app_list(full=False):
|
||||||
|
|
||||||
def app_info(app, full=False):
|
def app_info(app, full=False):
|
||||||
"""
|
"""
|
||||||
Get app info
|
Get info for a specific app
|
||||||
|
|
||||||
Keyword argument:
|
|
||||||
app -- Specific app ID
|
|
||||||
raw -- Return the full app_dict
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not _is_installed(app):
|
if not _is_installed(app):
|
||||||
raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id())
|
raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id())
|
||||||
|
@ -133,7 +144,7 @@ def app_info(app, full=False):
|
||||||
ret['settings'] = settings
|
ret['settings'] = settings
|
||||||
|
|
||||||
absolute_app_name = app if "__" not in app else app[:app.index('__')] # idk this is the name of the app even for multiinstance apps (so wordpress__2 -> wordpress)
|
absolute_app_name = app if "__" not in app else app[:app.index('__')] # idk this is the name of the app even for multiinstance apps (so wordpress__2 -> wordpress)
|
||||||
ret["from_catalog"] = _load_apps_catalog().get(absolute_app_name, {})
|
ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {})
|
||||||
ret['upgradable'] = _app_upgradable(ret)
|
ret['upgradable'] = _app_upgradable(ret)
|
||||||
ret['supports_change_url'] = os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url"))
|
ret['supports_change_url'] = os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url"))
|
||||||
ret['supports_backup_restore'] = (os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "backup")) and
|
ret['supports_backup_restore'] = (os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "backup")) and
|
||||||
|
@ -609,7 +620,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
|
||||||
if answer.upper() != "Y":
|
if answer.upper() != "Y":
|
||||||
raise YunohostError("aborting")
|
raise YunohostError("aborting")
|
||||||
|
|
||||||
raw_app_list = _load_apps_catalog()
|
raw_app_list = _load_apps_catalog()["apps"]
|
||||||
|
|
||||||
if app in raw_app_list or ('@' in app) or ('http://' in app) or ('https://' in app):
|
if app in raw_app_list or ('@' in app) or ('http://' in app) or ('https://' in app):
|
||||||
|
|
||||||
|
@ -2108,7 +2119,7 @@ def _fetch_app_from_git(app):
|
||||||
else:
|
else:
|
||||||
manifest['remote']['revision'] = revision
|
manifest['remote']['revision'] = revision
|
||||||
else:
|
else:
|
||||||
app_dict = _load_apps_catalog()
|
app_dict = _load_apps_catalog()["apps"]
|
||||||
|
|
||||||
if app not in app_dict:
|
if app not in app_dict:
|
||||||
raise YunohostError('app_unknown')
|
raise YunohostError('app_unknown')
|
||||||
|
@ -2645,11 +2656,14 @@ def _update_apps_catalog():
|
||||||
|
|
||||||
def _load_apps_catalog():
|
def _load_apps_catalog():
|
||||||
"""
|
"""
|
||||||
Read all the apps catalog cache files and build a single dict (app_dict)
|
Read all the apps catalog cache files and build a single dict (merged_catalog)
|
||||||
corresponding to all known apps in all indexes
|
corresponding to all known apps and categories
|
||||||
"""
|
"""
|
||||||
|
|
||||||
app_dict = {}
|
merged_catalog = {
|
||||||
|
"apps": {},
|
||||||
|
"categories": []
|
||||||
|
}
|
||||||
|
|
||||||
for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]:
|
for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]:
|
||||||
|
|
||||||
|
@ -2672,18 +2686,22 @@ def _load_apps_catalog():
|
||||||
del apps_catalog_content["from_api_version"]
|
del apps_catalog_content["from_api_version"]
|
||||||
|
|
||||||
# Add apps from this catalog to the output
|
# Add apps from this catalog to the output
|
||||||
for app, info in apps_catalog_content.items():
|
for app, info in apps_catalog_content["apps"].items():
|
||||||
|
|
||||||
# (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ...
|
# (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ...
|
||||||
# in which case we keep only the first one found)
|
# in which case we keep only the first one found)
|
||||||
if app in app_dict:
|
if app in merged_catalog["apps"]:
|
||||||
logger.warning("Duplicate app %s found between apps catalog %s and %s" % (app, apps_catalog_id, app_dict[app]['repository']))
|
logger.warning("Duplicate app %s found between apps catalog %s and %s"
|
||||||
|
% (app, apps_catalog_id, merged_catalog["apps"][app]['repository']))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
info['repository'] = apps_catalog_id
|
info['repository'] = apps_catalog_id
|
||||||
app_dict[app] = info
|
merged_catalog["apps"][app] = info
|
||||||
|
|
||||||
return app_dict
|
# Annnnd categories
|
||||||
|
merged_catalog["categories"] += apps_catalog_content["categories"]
|
||||||
|
|
||||||
|
return merged_catalog
|
||||||
|
|
||||||
#
|
#
|
||||||
# ############################### #
|
# ############################### #
|
||||||
|
|
|
@ -14,6 +14,7 @@ from yunohost.app import (_initialize_apps_catalog_system,
|
||||||
_update_apps_catalog,
|
_update_apps_catalog,
|
||||||
_actual_apps_catalog_api_url,
|
_actual_apps_catalog_api_url,
|
||||||
_load_apps_catalog,
|
_load_apps_catalog,
|
||||||
|
app_catalog,
|
||||||
logger,
|
logger,
|
||||||
APPS_CATALOG_CACHE,
|
APPS_CATALOG_CACHE,
|
||||||
APPS_CATALOG_CONF,
|
APPS_CATALOG_CONF,
|
||||||
|
@ -25,8 +26,14 @@ APPS_CATALOG_DEFAULT_URL_FULL = _actual_apps_catalog_api_url(APPS_CATALOG_DEFAUL
|
||||||
CRON_FOLDER, CRON_NAME = APPS_CATALOG_CRON_PATH.rsplit("/", 1)
|
CRON_FOLDER, CRON_NAME = APPS_CATALOG_CRON_PATH.rsplit("/", 1)
|
||||||
|
|
||||||
DUMMY_APP_CATALOG = """{
|
DUMMY_APP_CATALOG = """{
|
||||||
"foo": {"id": "foo", "level": 4},
|
"apps": {
|
||||||
"bar": {"id": "bar", "level": 7}
|
"foo": {"id": "foo", "level": 4, "category": "yolo", "manifest":{"description": "Foo"}},
|
||||||
|
"bar": {"id": "bar", "level": 7, "category": "swag", "manifest":{"description": "Bar"}}
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
{"id": "yolo", "description": "YoLo"},
|
||||||
|
{"id": "swag", "description": "sWaG"}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -107,7 +114,7 @@ def test_apps_catalog_emptylist():
|
||||||
assert not len(apps_catalog_list)
|
assert not len(apps_catalog_list)
|
||||||
|
|
||||||
|
|
||||||
def test_apps_catalog_update_success(mocker):
|
def test_apps_catalog_update_nominal(mocker):
|
||||||
|
|
||||||
# Initialize ...
|
# Initialize ...
|
||||||
_initialize_apps_catalog_system()
|
_initialize_apps_catalog_system()
|
||||||
|
@ -130,9 +137,16 @@ def test_apps_catalog_update_success(mocker):
|
||||||
# Cache shouldn't be empty anymore empty
|
# Cache shouldn't be empty anymore empty
|
||||||
assert glob.glob(APPS_CATALOG_CACHE + "/*")
|
assert glob.glob(APPS_CATALOG_CACHE + "/*")
|
||||||
|
|
||||||
app_dict = _load_apps_catalog()
|
# And if we load the catalog, we sould find
|
||||||
assert "foo" in app_dict.keys()
|
# - foo and bar as apps (unordered),
|
||||||
assert "bar" in app_dict.keys()
|
# - yolo and swag as categories (ordered)
|
||||||
|
catalog = app_catalog(with_categories=True)
|
||||||
|
|
||||||
|
assert "apps" in catalog
|
||||||
|
assert set(catalog["apps"].keys()) == set(["foo", "bar"])
|
||||||
|
|
||||||
|
assert "categories" in catalog
|
||||||
|
assert [c["id"] for c in catalog["categories"]] == ["yolo", "swag"]
|
||||||
|
|
||||||
|
|
||||||
def test_apps_catalog_update_404(mocker):
|
def test_apps_catalog_update_404(mocker):
|
||||||
|
@ -219,7 +233,7 @@ def test_apps_catalog_load_with_empty_cache(mocker):
|
||||||
# Try to load the apps catalog
|
# Try to load the apps catalog
|
||||||
# This should implicitly trigger an update in the background
|
# This should implicitly trigger an update in the background
|
||||||
mocker.spy(m18n, "n")
|
mocker.spy(m18n, "n")
|
||||||
app_dict = _load_apps_catalog()
|
app_dict = _load_apps_catalog()["apps"]
|
||||||
m18n.n.assert_any_call("apps_catalog_obsolete_cache")
|
m18n.n.assert_any_call("apps_catalog_obsolete_cache")
|
||||||
m18n.n.assert_any_call("apps_catalog_update_success")
|
m18n.n.assert_any_call("apps_catalog_update_success")
|
||||||
|
|
||||||
|
@ -252,7 +266,7 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker):
|
||||||
# Try to load the apps catalog
|
# Try to load the apps catalog
|
||||||
# This should implicitly trigger an update in the background
|
# This should implicitly trigger an update in the background
|
||||||
mocker.spy(logger, "warning")
|
mocker.spy(logger, "warning")
|
||||||
app_dict = _load_apps_catalog()
|
app_dict = _load_apps_catalog()["apps"]
|
||||||
logger.warning.assert_any_call(AnyStringWith("Duplicate"))
|
logger.warning.assert_any_call(AnyStringWith("Duplicate"))
|
||||||
|
|
||||||
# Cache shouldn't be empty anymore empty
|
# Cache shouldn't be empty anymore empty
|
||||||
|
@ -291,7 +305,7 @@ def test_apps_catalog_load_with_oudated_api_version(mocker):
|
||||||
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
|
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
|
||||||
|
|
||||||
mocker.spy(m18n, "n")
|
mocker.spy(m18n, "n")
|
||||||
app_dict = _load_apps_catalog()
|
app_dict = _load_apps_catalog()["apps"]
|
||||||
m18n.n.assert_any_call("apps_catalog_update_success")
|
m18n.n.assert_any_call("apps_catalog_update_success")
|
||||||
|
|
||||||
assert "foo" in app_dict.keys()
|
assert "foo" in app_dict.keys()
|
||||||
|
@ -329,7 +343,7 @@ def test_apps_catalog_migrate_legacy_explicitly():
|
||||||
assert cron_job_is_there()
|
assert cron_job_is_there()
|
||||||
|
|
||||||
# Reading the apps_catalog should work
|
# Reading the apps_catalog should work
|
||||||
app_dict = _load_apps_catalog()
|
app_dict = _load_apps_catalog()["apps"]
|
||||||
assert "foo" in app_dict.keys()
|
assert "foo" in app_dict.keys()
|
||||||
assert "bar" in app_dict.keys()
|
assert "bar" in app_dict.keys()
|
||||||
|
|
||||||
|
@ -343,7 +357,7 @@ def test_apps_catalog_migrate_legacy_implicitly():
|
||||||
|
|
||||||
with requests_mock.Mocker() as m:
|
with requests_mock.Mocker() as m:
|
||||||
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
|
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
|
||||||
app_dict = _load_apps_catalog()
|
app_dict = _load_apps_catalog()["apps"]
|
||||||
|
|
||||||
assert "foo" in app_dict.keys()
|
assert "foo" in app_dict.keys()
|
||||||
assert "bar" in app_dict.keys()
|
assert "bar" in app_dict.keys()
|
||||||
|
|
Loading…
Add table
Reference in a new issue