diff --git a/src/ahriman/application/handlers/search.py b/src/ahriman/application/handlers/search.py
index 846b7eba..2c58b314 100644
--- a/src/ahriman/application/handlers/search.py
+++ b/src/ahriman/application/handlers/search.py
@@ -23,7 +23,8 @@ from dataclasses import fields
from typing import Callable, Iterable, List, Tuple, Type
from ahriman.application.handlers.handler import Handler
-from ahriman.core.alpm.aur import AUR
+from ahriman.core.alpm.remote.aur import AUR
+from ahriman.core.alpm.remote.official import Official
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidOption
from ahriman.core.formatters.aur_printer import AurPrinter
@@ -50,10 +51,14 @@ class Search(Handler):
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
"""
- packages_list = AUR.multisearch(*args.search)
- Search.check_if_empty(args.exit_code, not packages_list)
- for package in Search.sort(packages_list, args.sort_by):
- AurPrinter(package).print(args.info)
+ official_packages_list = Official.multisearch(*args.search)
+ aur_packages_list = AUR.multisearch(*args.search)
+ Search.check_if_empty(args.exit_code, not official_packages_list and not aur_packages_list)
+
+ for packages_list in (official_packages_list, aur_packages_list):
+ # keep sorting by packages source
+ for package in Search.sort(packages_list, args.sort_by):
+ AurPrinter(package).print(args.info)
@staticmethod
def sort(packages: Iterable[AURPackage], sort_by: str) -> List[AURPackage]:
diff --git a/src/ahriman/core/alpm/remote/__init__.py b/src/ahriman/core/alpm/remote/__init__.py
new file mode 100644
index 00000000..680676f1
--- /dev/null
+++ b/src/ahriman/core/alpm/remote/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021-2022 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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, see .
+#
diff --git a/src/ahriman/core/alpm/aur.py b/src/ahriman/core/alpm/remote/aur.py
similarity index 67%
rename from src/ahriman/core/alpm/aur.py
rename to src/ahriman/core/alpm/remote/aur.py
index eacde36a..01b7fe1a 100644
--- a/src/ahriman/core/alpm/aur.py
+++ b/src/ahriman/core/alpm/remote/aur.py
@@ -17,24 +17,21 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from __future__ import annotations
-
-import logging
import requests
-from typing import Any, Dict, List, Optional, Type
+from typing import Any, Dict, List, Optional
+from ahriman.core.alpm.remote.remote import Remote
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage
-class AUR:
+class AUR(Remote):
"""
AUR RPC wrapper
:cvar DEFAULT_RPC_URL: default AUR RPC url
:cvar DEFAULT_RPC_VERSION: default AUR RPC version
- :ivar logger: class logger
:ivar rpc_url: AUR RPC url
:ivar rpc_version: AUR RPC version
"""
@@ -48,46 +45,9 @@ class AUR:
:param rpc_url: AUR RPC url
:param rpc_version: AUR RPC version
"""
+ Remote.__init__(self)
self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
self.rpc_version = rpc_version or self.DEFAULT_RPC_VERSION
- self.logger = logging.getLogger("build_details")
-
- @classmethod
- def info(cls: Type[AUR], package_name: str) -> AURPackage:
- """
- get package info by its name
- :param package_name: package name to search
- :return: package which match the package name
- """
- return cls().package_info(package_name)
-
- @classmethod
- def multisearch(cls: Type[AUR], *keywords: str) -> List[AURPackage]:
- """
- search in AUR by using API with multiple words. This method is required in order to handle
- https://bugs.archlinux.org/task/49133. In addition short words will be dropped
- :param keywords: search terms, e.g. "ahriman", "is", "cool"
- :return: list of packages each of them matches all search terms
- """
- instance = cls()
- packages: Dict[str, AURPackage] = {}
- for term in filter(lambda word: len(word) > 3, keywords):
- portion = instance.search(term)
- packages = {
- package.package_base: package
- for package in portion
- if package.package_base in packages or not packages
- }
- return list(packages.values())
-
- @classmethod
- def search(cls: Type[AUR], *keywords: str) -> List[AURPackage]:
- """
- search package in AUR web
- :param keywords: keywords to search
- :return: list of packages which match the criteria
- """
- return cls().package_search(*keywords)
@staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
@@ -144,11 +104,10 @@ class AUR:
packages = self.make_request("info", package_name)
return next(package for package in packages if package.name == package_name)
- def package_search(self, *keywords: str, by: str = "name-desc") -> List[AURPackage]:
+ def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
- :param by: search by the field
:return: list of packages which match the criteria
"""
- return self.make_request("search", *keywords, by=by)
+ return self.make_request("search", *keywords, by="name-desc")
diff --git a/src/ahriman/core/alpm/remote/official.py b/src/ahriman/core/alpm/remote/official.py
new file mode 100644
index 00000000..3e20ad25
--- /dev/null
+++ b/src/ahriman/core/alpm/remote/official.py
@@ -0,0 +1,91 @@
+#
+# Copyright (c) 2021-2022 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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, see .
+#
+import requests
+
+from typing import Any, Dict, List, Optional
+
+from ahriman.core.alpm.remote.remote import Remote
+from ahriman.core.exceptions import InvalidPackageInfo
+from ahriman.core.util import exception_response_text
+from ahriman.models.aur_package import AURPackage
+
+
+class Official(Remote):
+ """
+ official repository RPC wrapper
+ :cvar DEFAULT_RPC_URL: default AUR RPC url
+ :ivar rpc_url: AUR RPC url
+ """
+
+ DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
+
+ def __init__(self, rpc_url: Optional[str] = None) -> None:
+ """
+ default constructor
+ :param rpc_url: AUR RPC url
+ """
+ Remote.__init__(self)
+ self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
+
+ @staticmethod
+ def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
+ """
+ parse RPC response to package list
+ :param response: RPC response json
+ :return: list of parsed packages
+ """
+ if not response["valid"]:
+ raise InvalidPackageInfo("API validation error")
+ return [AURPackage.from_repo(package) for package in response["results"]]
+
+ def make_request(self, *args: str, by: str) -> List[AURPackage]:
+ """
+ perform request to official repositories RPC
+ :param args: list of arguments to be passed as args query parameter
+ :param by: search by the field
+ :return: response parsed to package list
+ """
+ try:
+ response = requests.get(self.rpc_url, params={by: args})
+ response.raise_for_status()
+ return self.parse_response(response.json())
+ except requests.HTTPError as e:
+ self.logger.exception("could not perform request: %s", exception_response_text(e))
+ raise
+ except Exception:
+ self.logger.exception("could not perform request")
+ raise
+
+ def package_info(self, package_name: str) -> AURPackage:
+ """
+ get package info by its name
+ :param package_name: package name to search
+ :return: package which match the package name
+ """
+ packages = self.make_request(package_name, by="name")
+ return next(package for package in packages if package.name == package_name)
+
+ def package_search(self, *keywords: str) -> List[AURPackage]:
+ """
+ search package in AUR web
+ :param keywords: keywords to search
+ :return: list of packages which match the criteria
+ """
+ return self.make_request(*keywords, by="q")
diff --git a/src/ahriman/core/alpm/remote/remote.py b/src/ahriman/core/alpm/remote/remote.py
new file mode 100644
index 00000000..7ac55488
--- /dev/null
+++ b/src/ahriman/core/alpm/remote/remote.py
@@ -0,0 +1,92 @@
+#
+# Copyright (c) 2021-2022 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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, see .
+#
+from __future__ import annotations
+
+import logging
+
+from typing import Dict, List, Type
+
+from ahriman.models.aur_package import AURPackage
+
+
+class Remote:
+ """
+ base class for remote package search
+ :ivar logger: class logger
+ """
+
+ def __init__(self) -> None:
+ """
+ default constructor
+ """
+ self.logger = logging.getLogger("build_details")
+
+ @classmethod
+ def info(cls: Type[Remote], package_name: str) -> AURPackage:
+ """
+ get package info by its name
+ :param package_name: package name to search
+ :return: package which match the package name
+ """
+ return cls().package_info(package_name)
+
+ @classmethod
+ def multisearch(cls: Type[Remote], *keywords: str) -> List[AURPackage]:
+ """
+ search in remote repository by using API with multiple words. This method is required in order to handle
+ https://bugs.archlinux.org/task/49133. In addition, short words will be dropped
+ :param keywords: search terms, e.g. "ahriman", "is", "cool"
+ :return: list of packages each of them matches all search terms
+ """
+ instance = cls()
+ packages: Dict[str, AURPackage] = {}
+ for term in filter(lambda word: len(word) > 3, keywords):
+ portion = instance.search(term)
+ packages = {
+ package.name: package # not mistake to group them by name
+ for package in portion
+ if package.name in packages or not packages
+ }
+ return list(packages.values())
+
+ @classmethod
+ def search(cls: Type[Remote], *keywords: str) -> List[AURPackage]:
+ """
+ search package in AUR web
+ :param keywords: keywords to search
+ :return: list of packages which match the criteria
+ """
+ return cls().package_search(*keywords)
+
+ def package_info(self, package_name: str) -> AURPackage:
+ """
+ get package info by its name
+ :param package_name: package name to search
+ :return: package which match the package name
+ """
+ raise NotImplementedError
+
+ def package_search(self, *keywords: str) -> List[AURPackage]:
+ """
+ search package in AUR web
+ :param keywords: keywords to search
+ :return: list of packages which match the criteria
+ """
+ raise NotImplementedError
diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py
index 7021a251..70bde8f7 100644
--- a/src/ahriman/core/util.py
+++ b/src/ahriman/core/util.py
@@ -116,6 +116,18 @@ def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str
return {key: value for key, value in source.items() if key in known_fields and value is not None}
+def full_version(epoch: Union[str, int, None], pkgver: str, pkgrel: str) -> str:
+ """
+ generate full version from components
+ :param epoch: package epoch if any
+ :param pkgver: package version
+ :param pkgrel: package release version (arch linux specific)
+ :return: generated version
+ """
+ prefix = f"{epoch}:" if epoch else ""
+ return f"{prefix}{pkgver}-{pkgrel}"
+
+
def package_like(filename: Path) -> bool:
"""
check if file looks like package
diff --git a/src/ahriman/models/aur_package.py b/src/ahriman/models/aur_package.py
index 487dce1e..de2514ff 100644
--- a/src/ahriman/models/aur_package.py
+++ b/src/ahriman/models/aur_package.py
@@ -25,7 +25,7 @@ import inflection
from dataclasses import dataclass, field, fields
from typing import Any, Callable, Dict, List, Optional, Type
-from ahriman.core.util import filter_json
+from ahriman.core.util import filter_json, full_version
@dataclass
@@ -59,12 +59,12 @@ class AURPackage:
package_base_id: int
package_base: str
version: str
- description: str
num_votes: int
popularity: float
first_submitted: datetime.datetime
last_modified: datetime.datetime
url_path: str
+ description: str = "" # despite the fact that the field is required some packages don't have it
url: Optional[str] = None
out_of_date: Optional[datetime.datetime] = None
maintainer: Optional[str] = None
@@ -88,6 +88,39 @@ class AURPackage:
properties = cls.convert(dump)
return cls(**filter_json(properties, known_fields))
+ @classmethod
+ def from_repo(cls: Type[AURPackage], dump: Dict[str, Any]) -> AURPackage:
+ """
+ construct package descriptor from official repository RPC properties
+ :param dump: json dump body
+ :return: AUR package descriptor
+ """
+ return cls(
+ id=0,
+ name=dump["pkgname"],
+ package_base_id=0,
+ package_base=dump["pkgbase"],
+ version=full_version(dump["epoch"], dump["pkgver"], dump["pkgrel"]),
+ description=dump["pkgdesc"],
+ num_votes=0,
+ popularity=0.0,
+ first_submitted=datetime.datetime.utcfromtimestamp(0),
+ last_modified=datetime.datetime.strptime(dump["last_update"], "%Y-%m-%dT%H:%M:%S.%fZ"),
+ url_path="",
+ url=dump["url"],
+ out_of_date=datetime.datetime.strptime(
+ dump["flag_date"],
+ "%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None,
+ maintainer=next(iter(dump["maintainers"]), None),
+ depends=dump["depends"],
+ make_depends=dump["makedepends"],
+ opt_depends=dump["optdepends"],
+ conflicts=dump["conflicts"],
+ provides=dump["provides"],
+ license=dump["licenses"],
+ keywords=[],
+ )
+
@staticmethod
def convert(descriptor: Dict[str, Any]) -> Dict[str, Any]:
"""
diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py
index 8c915f30..9c723bdb 100644
--- a/src/ahriman/models/package.py
+++ b/src/ahriman/models/package.py
@@ -26,12 +26,13 @@ from dataclasses import asdict, dataclass
from pathlib import Path
from pyalpm import vercmp # type: ignore
from srcinfo.parse import parse_srcinfo # type: ignore
-from typing import Any, Dict, Iterable, List, Optional, Set, Type
+from typing import Any, Dict, Iterable, List, Set, Type
-from ahriman.core.alpm.aur import AUR
from ahriman.core.alpm.pacman import Pacman
+from ahriman.core.alpm.remote.aur import AUR
+from ahriman.core.alpm.remote.official import Official
from ahriman.core.exceptions import InvalidPackageInfo
-from ahriman.core.util import check_output
+from ahriman.core.util import check_output, full_version
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@@ -144,7 +145,7 @@ class Package:
if errors:
raise InvalidPackageInfo(errors)
packages = {key: PackageDescription() for key in srcinfo["packages"]}
- version = cls.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
+ version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
return cls(srcinfo["pkgbase"], version, aur_url, packages)
@@ -165,6 +166,17 @@ class Package:
aur_url=dump["aur_url"],
packages=packages)
+ @classmethod
+ def from_official(cls: Type[Package], name: str, aur_url: str) -> Package:
+ """
+ construct package properties from official repository page
+ :param name: package name (either base or normal name)
+ :param aur_url: AUR root url
+ :return: package properties
+ """
+ package = Official.info(name)
+ return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
+
@classmethod
def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package:
"""
@@ -183,6 +195,8 @@ class Package:
return cls.from_aur(package, aur_url)
if resolved_source == PackageSource.Local:
return cls.from_build(Path(package), aur_url)
+ if resolved_source == PackageSource.Repository:
+ return cls.from_official(package, aur_url)
raise InvalidPackageInfo(f"Unsupported local package source {resolved_source}")
except InvalidPackageInfo:
raise
@@ -215,18 +229,6 @@ class Package:
full_list = set(depends + makedepends) - packages
return {trim_version(package_name) for package_name in full_list}
- @staticmethod
- def full_version(epoch: Optional[str], pkgver: str, pkgrel: str) -> str:
- """
- generate full version from components
- :param epoch: package epoch if any
- :param pkgver: package version
- :param pkgrel: package release version (arch linux specific)
- :return: generated version
- """
- prefix = f"{epoch}:" if epoch else ""
- return f"{prefix}{pkgver}-{pkgrel}"
-
def actual_version(self, paths: RepositoryPaths) -> str:
"""
additional method to handle VCS package versions
@@ -252,7 +254,7 @@ class Package:
if errors:
raise InvalidPackageInfo(errors)
- return self.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
+ return full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
except Exception:
logger.exception("cannot determine version of VCS package, make sure that you have VCS tools installed")
diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py
index 0a50d3a6..3d222e67 100644
--- a/src/ahriman/web/views/service/search.py
+++ b/src/ahriman/web/views/service/search.py
@@ -20,7 +20,7 @@
from aiohttp.web import HTTPNotFound, Response, json_response
from typing import Callable, List
-from ahriman.core.alpm.aur import AUR
+from ahriman.core.alpm.remote.aur import AUR
from ahriman.models.aur_package import AURPackage
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
diff --git a/tests/ahriman/application/handlers/test_handler_search.py b/tests/ahriman/application/handlers/test_handler_search.py
index 3ec8bbe8..d5996d60 100644
--- a/tests/ahriman/application/handlers/test_handler_search.py
+++ b/tests/ahriman/application/handlers/test_handler_search.py
@@ -3,6 +3,7 @@ import dataclasses
import pytest
from pytest_mock import MockerFixture
+from unittest import mock
from ahriman.application.handlers import Search
from ahriman.core.configuration import Configuration
@@ -29,14 +30,17 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package
must run command
"""
args = _default_args(args)
- search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
+ aur_search_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
+ official_search_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch",
+ return_value=[aur_package_ahriman])
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty")
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Search.run(args, "x86_64", configuration, True, False)
- search_mock.assert_called_once_with("ahriman")
+ aur_search_mock.assert_called_once_with("ahriman")
+ official_search_mock.assert_called_once_with("ahriman")
check_mock.assert_called_once_with(False, False)
- print_mock.assert_called_once_with(False)
+ print_mock.assert_has_calls([mock.call(False), mock.call(False)])
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@@ -45,7 +49,8 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
"""
args = _default_args(args)
args.exit_code = True
- mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[])
+ mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[])
+ mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[])
mocker.patch("ahriman.core.formatters.printer.Printer.print")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty")
@@ -59,11 +64,15 @@ def test_run_sort(args: argparse.Namespace, configuration: Configuration, aur_pa
must run command with sorting
"""
args = _default_args(args)
- mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
+ mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
+ mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[])
sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
Search.run(args, "x86_64", configuration, True, False)
- sort_mock.assert_called_once_with([aur_package_ahriman], "name")
+ sort_mock.assert_has_calls([
+ mock.call([], "name"), mock.call().__iter__(),
+ mock.call([aur_package_ahriman], "name"), mock.call().__iter__()
+ ])
def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: AURPackage,
@@ -73,11 +82,15 @@ def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur
"""
args = _default_args(args)
args.sort_by = "field"
- mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
+ mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
+ mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[])
sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
Search.run(args, "x86_64", configuration, True, False)
- sort_mock.assert_called_once_with([aur_package_ahriman], "field")
+ sort_mock.assert_has_calls([
+ mock.call([], "field"), mock.call().__iter__(),
+ mock.call([aur_package_ahriman], "field"), mock.call().__iter__()
+ ])
def test_sort(aur_package_ahriman: AURPackage) -> None:
diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py
index 301edbda..a02c633a 100644
--- a/tests/ahriman/conftest.py
+++ b/tests/ahriman/conftest.py
@@ -124,6 +124,50 @@ def aur_package_ahriman() -> AURPackage:
)
+@pytest.fixture
+def aur_package_akonadi() -> AURPackage:
+ """
+ fixture for AUR package
+ :return: AUR package test instance
+ """
+ return AURPackage(
+ id=0,
+ name="akonadi",
+ package_base_id=0,
+ package_base="akonadi",
+ version="21.12.3-2",
+ description="PIM layer, which provides an asynchronous API to access all kind of PIM data",
+ num_votes=0,
+ popularity=0,
+ first_submitted=datetime.datetime(1970, 1, 1, 0, 0, 0),
+ last_modified=datetime.datetime(2022, 3, 6, 8, 39, 50, 610000),
+ url_path="",
+ url="https://kontact.kde.org",
+ out_of_date=None,
+ maintainer="felixonmars",
+ depends=[
+ "libakonadi",
+ "mariadb",
+ ],
+ make_depends=[
+ "boost",
+ "doxygen",
+ "extra-cmake-modules",
+ "kaccounts-integration",
+ "kitemmodels",
+ "postgresql",
+ "qt5-tools",
+ ],
+ opt_depends=[
+ "postgresql: PostgreSQL backend",
+ ],
+ conflicts=[],
+ provides=[],
+ license=["LGPL"],
+ keywords=[],
+ )
+
+
@pytest.fixture
def auth(configuration: Configuration) -> Auth:
"""
diff --git a/tests/ahriman/core/alpm/conftest.py b/tests/ahriman/core/alpm/conftest.py
deleted file mode 100644
index 6f0a021a..00000000
--- a/tests/ahriman/core/alpm/conftest.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import pytest
-
-from ahriman.core.alpm.aur import AUR
-
-
-@pytest.fixture
-def aur() -> AUR:
- """
- aur helper fixture
- :return: aur helper instance
- """
- return AUR()
diff --git a/tests/ahriman/core/alpm/remote/conftest.py b/tests/ahriman/core/alpm/remote/conftest.py
new file mode 100644
index 00000000..d94a04ef
--- /dev/null
+++ b/tests/ahriman/core/alpm/remote/conftest.py
@@ -0,0 +1,32 @@
+import pytest
+
+from ahriman.core.alpm.remote.aur import AUR
+from ahriman.core.alpm.remote.official import Official
+from ahriman.core.alpm.remote.remote import Remote
+
+
+@pytest.fixture
+def aur() -> AUR:
+ """
+ aur helper fixture
+ :return: aur helper instance
+ """
+ return AUR()
+
+
+@pytest.fixture
+def official() -> Official:
+ """
+ official repository fixture
+ :return: official repository helper instance
+ """
+ return Official()
+
+
+@pytest.fixture
+def remote() -> Remote:
+ """
+ official repository fixture
+ :return: official repository helper instance
+ """
+ return Remote()
diff --git a/tests/ahriman/core/alpm/test_aur.py b/tests/ahriman/core/alpm/remote/test_aur.py
similarity index 68%
rename from tests/ahriman/core/alpm/test_aur.py
rename to tests/ahriman/core/alpm/remote/test_aur.py
index bf9f9541..2d5e14c9 100644
--- a/tests/ahriman/core/alpm/test_aur.py
+++ b/tests/ahriman/core/alpm/remote/test_aur.py
@@ -4,10 +4,9 @@ import requests
from pathlib import Path
from pytest_mock import MockerFixture
-from unittest import mock
from unittest.mock import MagicMock
-from ahriman.core.alpm.aur import AUR
+from ahriman.core.alpm.remote.aur import AUR
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.models.aur_package import AURPackage
@@ -21,55 +20,6 @@ def _get_response(resource_path_root: Path) -> str:
return (resource_path_root / "models" / "package_ahriman_aur").read_text()
-def test_info(mocker: MockerFixture) -> None:
- """
- must call info method
- """
- info_mock = mocker.patch("ahriman.core.alpm.aur.AUR.package_info")
- AUR.info("ahriman")
- info_mock.assert_called_once_with("ahriman")
-
-
-def test_multisearch(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
- """
- must search in AUR with multiple words
- """
- terms = ["ahriman", "is", "cool"]
- search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.search", return_value=[aur_package_ahriman])
-
- assert AUR.multisearch(*terms) == [aur_package_ahriman]
- search_mock.assert_has_calls([mock.call("ahriman"), mock.call("cool")])
-
-
-def test_multisearch_empty(mocker: MockerFixture) -> None:
- """
- must return empty list if no long terms supplied
- """
- terms = ["it", "is"]
- search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.search")
-
- assert AUR.multisearch(*terms) == []
- search_mock.assert_not_called()
-
-
-def test_multisearch_single(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
- """
- must search in AUR with one word
- """
- search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.search", return_value=[aur_package_ahriman])
- assert AUR.multisearch("ahriman") == [aur_package_ahriman]
- search_mock.assert_called_once_with("ahriman")
-
-
-def test_search(mocker: MockerFixture) -> None:
- """
- must call search method
- """
- search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.package_search")
- AUR.search("ahriman")
- search_mock.assert_called_once_with("ahriman")
-
-
def test_parse_response(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
"""
must parse success response
@@ -87,7 +37,7 @@ def test_parse_response_error(resource_path_root: Path) -> None:
AUR.parse_response(json.loads(response))
-def test_parse_response_unknown_error(resource_path_root: Path) -> None:
+def test_parse_response_unknown_error() -> None:
"""
must raise exception on invalid response with empty error message
"""
@@ -159,7 +109,7 @@ def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerF
"""
must make request for info
"""
- request_mock = mocker.patch("ahriman.core.alpm.aur.AUR.make_request", return_value=[aur_package_ahriman])
+ request_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.make_request", return_value=[aur_package_ahriman])
assert aur.package_info(aur_package_ahriman.name) == aur_package_ahriman
request_mock.assert_called_once_with("info", aur_package_ahriman.name)
@@ -168,6 +118,6 @@ def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, mocker: Mocke
"""
must make request for search
"""
- request_mock = mocker.patch("ahriman.core.alpm.aur.AUR.make_request", return_value=[aur_package_ahriman])
- assert aur.package_search(aur_package_ahriman.name, by="name") == [aur_package_ahriman]
- request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name")
+ request_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.make_request", return_value=[aur_package_ahriman])
+ assert aur.package_search(aur_package_ahriman.name) == [aur_package_ahriman]
+ request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc")
diff --git a/tests/ahriman/core/alpm/remote/test_official.py b/tests/ahriman/core/alpm/remote/test_official.py
new file mode 100644
index 00000000..e34376b7
--- /dev/null
+++ b/tests/ahriman/core/alpm/remote/test_official.py
@@ -0,0 +1,88 @@
+import json
+import pytest
+import requests
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+from unittest.mock import MagicMock
+
+from ahriman.core.alpm.remote.official import Official
+from ahriman.core.exceptions import InvalidPackageInfo
+from ahriman.models.aur_package import AURPackage
+
+
+def _get_response(resource_path_root: Path) -> str:
+ """
+ load response from resource file
+ :param resource_path_root: path to resource root
+ :return: response text
+ """
+ return (resource_path_root / "models" / "package_akonadi_aur").read_text()
+
+
+def test_parse_response(aur_package_akonadi: AURPackage, resource_path_root: Path) -> None:
+ """
+ must parse success response
+ """
+ response = _get_response(resource_path_root)
+ assert Official.parse_response(json.loads(response)) == [aur_package_akonadi]
+
+
+def test_parse_response_unknown_error(resource_path_root: Path) -> None:
+ """
+ must raise exception on invalid response with empty error message
+ """
+ response = (resource_path_root / "models" / "official_error").read_text()
+ with pytest.raises(InvalidPackageInfo, match="API validation error"):
+ Official.parse_response(json.loads(response))
+
+
+def test_make_request(official: Official, aur_package_akonadi: AURPackage,
+ mocker: MockerFixture, resource_path_root: Path) -> None:
+ """
+ must perform request to official repositories
+ """
+ response_mock = MagicMock()
+ response_mock.json.return_value = json.loads(_get_response(resource_path_root))
+ request_mock = mocker.patch("requests.get", return_value=response_mock)
+
+ assert official.make_request("akonadi", by="q") == [aur_package_akonadi]
+ request_mock.assert_called_once_with("https://archlinux.org/packages/search/json", params={"q": ("akonadi",)})
+
+
+def test_make_request_failed(official: Official, mocker: MockerFixture) -> None:
+ """
+ must reraise generic exception
+ """
+ mocker.patch("requests.get", side_effect=Exception())
+ with pytest.raises(Exception):
+ official.make_request("akonadi", by="q")
+
+
+def test_make_request_failed_http_error(official: Official, mocker: MockerFixture) -> None:
+ """
+ must reraise http exception
+ """
+ mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
+ with pytest.raises(requests.exceptions.HTTPError):
+ official.make_request("akonadi", by="q")
+
+
+def test_package_info(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None:
+ """
+ must make request for info
+ """
+ request_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.make_request",
+ return_value=[aur_package_akonadi])
+ assert official.package_info(aur_package_akonadi.name) == aur_package_akonadi
+ request_mock.assert_called_once_with(aur_package_akonadi.name, by="name")
+
+
+def test_package_search(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None:
+ """
+ must make request for search
+ """
+ request_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.make_request",
+ return_value=[aur_package_akonadi])
+ assert official.package_search(aur_package_akonadi.name) == [aur_package_akonadi]
+ request_mock.assert_called_once_with(aur_package_akonadi.name, by="q")
diff --git a/tests/ahriman/core/alpm/remote/test_remote.py b/tests/ahriman/core/alpm/remote/test_remote.py
new file mode 100644
index 00000000..a23e84d7
--- /dev/null
+++ b/tests/ahriman/core/alpm/remote/test_remote.py
@@ -0,0 +1,72 @@
+import pytest
+
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.core.alpm.remote.remote import Remote
+from ahriman.models.aur_package import AURPackage
+
+
+def test_info(mocker: MockerFixture) -> None:
+ """
+ must call info method
+ """
+ info_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.package_info")
+ Remote.info("ahriman")
+ info_mock.assert_called_once_with("ahriman")
+
+
+def test_multisearch(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
+ """
+ must search in AUR with multiple words
+ """
+ terms = ["ahriman", "is", "cool"]
+ search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search", return_value=[aur_package_ahriman])
+
+ assert Remote.multisearch(*terms) == [aur_package_ahriman]
+ search_mock.assert_has_calls([mock.call("ahriman"), mock.call("cool")])
+
+
+def test_multisearch_empty(mocker: MockerFixture) -> None:
+ """
+ must return empty list if no long terms supplied
+ """
+ terms = ["it", "is"]
+ search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search")
+
+ assert Remote.multisearch(*terms) == []
+ search_mock.assert_not_called()
+
+
+def test_multisearch_single(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
+ """
+ must search in AUR with one word
+ """
+ search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search", return_value=[aur_package_ahriman])
+ assert Remote.multisearch("ahriman") == [aur_package_ahriman]
+ search_mock.assert_called_once_with("ahriman")
+
+
+def test_search(mocker: MockerFixture) -> None:
+ """
+ must call search method
+ """
+ search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.package_search")
+ Remote.search("ahriman")
+ search_mock.assert_called_once_with("ahriman")
+
+
+def test_package_info(remote: Remote) -> None:
+ """
+ must raise NotImplemented for missing package info method
+ """
+ with pytest.raises(NotImplementedError):
+ remote.package_info("package")
+
+
+def test_package_search(remote: Remote) -> None:
+ """
+ must raise NotImplemented for missing package search method
+ """
+ with pytest.raises(NotImplementedError):
+ remote.package_search("package")
diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py
index a748b4c9..4248fb62 100644
--- a/tests/ahriman/core/test_util.py
+++ b/tests/ahriman/core/test_util.py
@@ -9,8 +9,8 @@ from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.exceptions import BuildFailed, InvalidOption, UnsafeRun
-from ahriman.core.util import check_output, check_user, exception_response_text, filter_json, package_like, \
- pretty_datetime, pretty_size, tmpdir, walk
+from ahriman.core.util import check_output, check_user, exception_response_text, filter_json, full_version, \
+ package_like, pretty_datetime, pretty_size, tmpdir, walk
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
@@ -177,6 +177,16 @@ def test_filter_json_empty_value(package_ahriman: Package) -> None:
assert "base" not in filter_json(probe, probe.keys())
+def test_full_version() -> None:
+ """
+ must construct full version
+ """
+ assert full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1"
+ assert full_version(None, "0.12.1", "1") == "0.12.1-1"
+ assert full_version(0, "0.12.1", "1") == "0.12.1-1"
+ assert full_version(1, "0.12.1", "1") == "1:0.12.1-1"
+
+
def test_package_like(package_ahriman: Package) -> None:
"""
package_like must return true for archives
@@ -298,24 +308,26 @@ def test_walk(resource_path_root: Path) -> None:
must traverse directory recursively
"""
expected = sorted([
- resource_path_root / "core/ahriman.ini",
- resource_path_root / "core/logging.ini",
- resource_path_root / "models/aur_error",
- resource_path_root / "models/big_file_checksum",
- resource_path_root / "models/empty_file_checksum",
- resource_path_root / "models/package_ahriman_aur",
- resource_path_root / "models/package_ahriman_srcinfo",
- resource_path_root / "models/package_tpacpi-bat-git_srcinfo",
- resource_path_root / "models/package_yay_srcinfo",
- resource_path_root / "web/templates/build-status/login-modal.jinja2",
- resource_path_root / "web/templates/build-status/package-actions-modals.jinja2",
- resource_path_root / "web/templates/build-status/package-actions-script.jinja2",
- resource_path_root / "web/templates/static/favicon.ico",
- resource_path_root / "web/templates/utils/bootstrap-scripts.jinja2",
- resource_path_root / "web/templates/utils/style.jinja2",
- resource_path_root / "web/templates/build-status.jinja2",
- resource_path_root / "web/templates/email-index.jinja2",
- resource_path_root / "web/templates/repo-index.jinja2",
+ resource_path_root / "core" / "ahriman.ini",
+ resource_path_root / "core" / "logging.ini",
+ resource_path_root / "models" / "aur_error",
+ resource_path_root / "models" / "big_file_checksum",
+ resource_path_root / "models" / "empty_file_checksum",
+ resource_path_root / "models" / "official_error",
+ resource_path_root / "models" / "package_ahriman_aur",
+ resource_path_root / "models" / "package_akonadi_aur",
+ resource_path_root / "models" / "package_ahriman_srcinfo",
+ resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo",
+ resource_path_root / "models" / "package_yay_srcinfo",
+ resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
+ resource_path_root / "web" / "templates" / "build-status" / "package-actions-modals.jinja2",
+ resource_path_root / "web" / "templates" / "build-status" / "package-actions-script.jinja2",
+ resource_path_root / "web" / "templates" / "static" / "favicon.ico",
+ resource_path_root / "web" / "templates" / "utils" / "bootstrap-scripts.jinja2",
+ resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
+ resource_path_root / "web" / "templates" / "build-status.jinja2",
+ resource_path_root / "web" / "templates" / "email-index.jinja2",
+ resource_path_root / "web" / "templates" / "repo-index.jinja2",
])
local_files = list(sorted(walk(resource_path_root)))
assert local_files == expected
diff --git a/tests/ahriman/models/test_aur_package.py b/tests/ahriman/models/test_aur_package.py
index 96029b42..c50dbee0 100644
--- a/tests/ahriman/models/test_aur_package.py
+++ b/tests/ahriman/models/test_aur_package.py
@@ -9,7 +9,7 @@ from typing import Any, Dict
from ahriman.models.aur_package import AURPackage
-def _get_data(resource_path_root: Path) -> Dict[str, Any]:
+def _get_aur_data(resource_path_root: Path) -> Dict[str, Any]:
"""
load package description from resource file
:param resource_path_root: path to resource root
@@ -19,11 +19,21 @@ def _get_data(resource_path_root: Path) -> Dict[str, Any]:
return json.loads(response)["results"][0]
+def _get_official_data(resource_path_root: Path) -> Dict[str, Any]:
+ """
+ load package description from resource file
+ :param resource_path_root: path to resource root
+ :return: json descriptor
+ """
+ response = (resource_path_root / "models" / "package_akonadi_aur").read_text()
+ return json.loads(response)["results"][0]
+
+
def test_from_json(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
"""
must load package from json
"""
- model = _get_data(resource_path_root)
+ model = _get_aur_data(resource_path_root)
assert AURPackage.from_json(model) == aur_package_ahriman
@@ -35,11 +45,19 @@ def test_from_json_2(aur_package_ahriman: AURPackage, mocker: MockerFixture) ->
assert AURPackage.from_json(asdict(aur_package_ahriman)) == aur_package_ahriman
+def test_from_repo(aur_package_akonadi: AURPackage, resource_path_root: Path) -> None:
+ """
+ must load package from repository api json
+ """
+ model = _get_official_data(resource_path_root)
+ assert AURPackage.from_repo(model) == aur_package_akonadi
+
+
def test_convert(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
"""
must convert fields to snakecase and also apply converters
"""
- model = _get_data(resource_path_root)
+ model = _get_aur_data(resource_path_root)
converted = AURPackage.convert(model)
known_fields = [pair.name for pair in fields(AURPackage)]
assert all(field in known_fields for field in converted)
diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py
index 61454993..2af0b70e 100644
--- a/tests/ahriman/models/test_package.py
+++ b/tests/ahriman/models/test_package.py
@@ -101,7 +101,7 @@ def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, moc
"""
must construct package from aur
"""
- mocker.patch("ahriman.core.alpm.aur.AUR.info", return_value=aur_package_ahriman)
+ mocker.patch("ahriman.core.alpm.remote.aur.AUR.info", return_value=aur_package_ahriman)
package = Package.from_aur(package_ahriman.base, package_ahriman.aur_url)
assert package_ahriman.base == package.base
@@ -154,6 +154,18 @@ def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None:
assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git
+def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
+ """
+ must construct package from official repository
+ """
+ mocker.patch("ahriman.core.alpm.remote.official.Official.info", return_value=aur_package_ahriman)
+
+ package = Package.from_official(package_ahriman.base, package_ahriman.aur_url)
+ assert package_ahriman.base == package.base
+ assert package_ahriman.version == package.version
+ assert package_ahriman.packages.keys() == package.packages.keys()
+
+
def test_load_resolve(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must resolve source before package loading
@@ -193,6 +205,15 @@ def test_load_from_build(package_ahriman: Package, pyalpm_handle: MagicMock, moc
load_mock.assert_called_once_with(Path("path"), package_ahriman.aur_url)
+def test_load_from_official(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
+ """
+ must load package from AUR
+ """
+ load_mock = mocker.patch("ahriman.models.package.Package.from_official")
+ Package.load("path", PackageSource.Repository, pyalpm_handle, package_ahriman.aur_url)
+ load_mock.assert_called_once_with("path", package_ahriman.aur_url)
+
+
def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must raise InvalidPackageInfo on exception
@@ -240,14 +261,6 @@ def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Pa
assert Package.dependencies(Path("path")) == {"git", "go", "pacman"}
-def test_full_version() -> None:
- """
- must construct full version
- """
- assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1"
- assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1"
-
-
def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
"""
must return same actual_version as version is
diff --git a/tests/ahriman/web/views/service/test_views_service_search.py b/tests/ahriman/web/views/service/test_views_service_search.py
index 7ba037e6..13eefa50 100644
--- a/tests/ahriman/web/views/service/test_views_service_search.py
+++ b/tests/ahriman/web/views/service/test_views_service_search.py
@@ -21,7 +21,7 @@ async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker:
"""
must call get request correctly
"""
- mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
+ mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
response = await client.get("/service-api/v1/search", params={"for": "ahriman"})
assert response.ok
@@ -33,7 +33,7 @@ async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise 400 on empty search string
"""
- search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[])
+ search_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[])
response = await client.get("/service-api/v1/search")
assert response.status == 404
@@ -44,7 +44,7 @@ async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
"""
must join search args with space
"""
- search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch")
+ search_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch")
response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")])
assert response.ok
diff --git a/tests/testresources/models/official_error b/tests/testresources/models/official_error
new file mode 100644
index 00000000..0f84c09b
--- /dev/null
+++ b/tests/testresources/models/official_error
@@ -0,0 +1,8 @@
+{
+ "version": 2,
+ "limit": 250,
+ "valid": false,
+ "results": [],
+ "num_pages": 1,
+ "page": 1
+}
diff --git a/tests/testresources/models/package_akonadi_aur b/tests/testresources/models/package_akonadi_aur
new file mode 100644
index 00000000..b58190fb
--- /dev/null
+++ b/tests/testresources/models/package_akonadi_aur
@@ -0,0 +1,55 @@
+{
+ "version": 2,
+ "limit": 250,
+ "valid": true,
+ "results": [
+ {
+ "pkgname": "akonadi",
+ "pkgbase": "akonadi",
+ "repo": "extra",
+ "arch": "x86_64",
+ "pkgver": "21.12.3",
+ "pkgrel": "2",
+ "epoch": 0,
+ "pkgdesc": "PIM layer, which provides an asynchronous API to access all kind of PIM data",
+ "url": "https://kontact.kde.org",
+ "filename": "akonadi-21.12.3-2-x86_64.pkg.tar.zst",
+ "compressed_size": 789510,
+ "installed_size": 2592656,
+ "build_date": "2022-03-04T11:50:03Z",
+ "last_update": "2022-03-06T08:39:50.610Z",
+ "flag_date": null,
+ "maintainers": [
+ "felixonmars",
+ "arojas"
+ ],
+ "packager": "arojas",
+ "groups": [],
+ "licenses": [
+ "LGPL"
+ ],
+ "conflicts": [],
+ "provides": [],
+ "replaces": [],
+ "depends": [
+ "libakonadi",
+ "mariadb"
+ ],
+ "optdepends": [
+ "postgresql: PostgreSQL backend"
+ ],
+ "makedepends": [
+ "boost",
+ "doxygen",
+ "extra-cmake-modules",
+ "kaccounts-integration",
+ "kitemmodels",
+ "postgresql",
+ "qt5-tools"
+ ],
+ "checkdepends": []
+ }
+ ],
+ "num_pages": 1,
+ "page": 1
+}