add support of officiall repositories api

This commit is contained in:
Evgenii Alekseev 2022-04-07 04:19:37 +03:00
parent 4990ce4198
commit 9ce1c36f35
22 changed files with 689 additions and 183 deletions

View File

@ -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]:

View File

@ -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 <http://www.gnu.org/licenses/>.
#

View File

@ -17,24 +17,21 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
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")

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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")

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -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

View File

@ -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]:
"""

View File

@ -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")

View File

@ -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

View File

@ -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:

View File

@ -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:
"""

View File

@ -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()

View File

@ -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()

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
{
"version": 2,
"limit": 250,
"valid": false,
"results": [],
"num_pages": 1,
"page": 1
}

View File

@ -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
}