mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-15 06:55:48 +00:00
use own aur wrapper (#49)
This commit is contained in:
@ -17,12 +17,11 @@
|
||||
# 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 aur # type: ignore
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from ahriman.application.formatters.printer import Printer
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
@ -31,7 +30,7 @@ class AurPrinter(Printer):
|
||||
print content of the AUR package
|
||||
"""
|
||||
|
||||
def __init__(self, package: aur.Package) -> None:
|
||||
def __init__(self, package: AURPackage) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param package: AUR package description
|
||||
@ -46,12 +45,12 @@ class AurPrinter(Printer):
|
||||
return [
|
||||
Property("Package base", self.content.package_base),
|
||||
Property("Description", self.content.description, is_required=True),
|
||||
Property("Upstream URL", self.content.url),
|
||||
Property("Licenses", self.content.license), # it should be actually a list
|
||||
Property("Maintainer", self.content.maintainer or ""), # I think it is optional
|
||||
Property("Upstream URL", self.content.url or ""),
|
||||
Property("Licenses", ",".join(self.content.license)),
|
||||
Property("Maintainer", self.content.maintainer or ""),
|
||||
Property("First submitted", pretty_datetime(self.content.first_submitted)),
|
||||
Property("Last updated", pretty_datetime(self.content.last_modified)),
|
||||
# more fields coming https://github.com/cdown/aur/pull/29
|
||||
Property("Keywords", ",".join(self.content.keywords)),
|
||||
]
|
||||
|
||||
def title(self) -> Optional[str]:
|
||||
|
@ -18,24 +18,27 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import aur # type: ignore
|
||||
|
||||
from dataclasses import fields
|
||||
from typing import Callable, Iterable, List, Tuple, Type
|
||||
|
||||
from ahriman.application.formatters.aur_printer import AurPrinter
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.alpm.aur import AUR
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
from ahriman.core.util import aur_search
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
|
||||
|
||||
class Search(Handler):
|
||||
"""
|
||||
packages search handler
|
||||
:cvar SORT_FIELDS: allowed fields to sort the package list
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
|
||||
SORT_FIELDS = set(aur.Package._fields) # later we will have to remove some fields from here (lists)
|
||||
# later we will have to remove some fields from here (lists)
|
||||
SORT_FIELDS = {pair.name for pair in fields(AURPackage)}
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
@ -47,12 +50,12 @@ class Search(Handler):
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
packages_list = aur_search(*args.search)
|
||||
packages_list = AUR.multisearch(*args.search)
|
||||
for package in Search.sort(packages_list, args.sort_by):
|
||||
AurPrinter(package).print(args.info)
|
||||
|
||||
@staticmethod
|
||||
def sort(packages: Iterable[aur.Package], sort_by: str) -> List[aur.Package]:
|
||||
def sort(packages: Iterable[AURPackage], sort_by: str) -> List[AURPackage]:
|
||||
"""
|
||||
sort package list by specified field
|
||||
:param packages: packages list to sort
|
||||
@ -63,6 +66,6 @@ class Search(Handler):
|
||||
raise InvalidOption(sort_by)
|
||||
# always sort by package name at the last
|
||||
# well technically it is not a string, but we can deal with it
|
||||
comparator: Callable[[aur.Package], Tuple[str, str]] =\
|
||||
comparator: Callable[[AURPackage], Tuple[str, str]] =\
|
||||
lambda package: (getattr(package, sort_by), package.name)
|
||||
return sorted(packages, key=comparator)
|
||||
|
152
src/ahriman/core/alpm/aur.py
Normal file
152
src/ahriman/core/alpm/aur.py
Normal file
@ -0,0 +1,152 @@
|
||||
#
|
||||
# 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
|
||||
import requests
|
||||
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from ahriman.core.exceptions import InvalidPackageInfo
|
||||
from ahriman.core.util import exception_response_text
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
|
||||
|
||||
class AUR:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
DEFAULT_RPC_URL = "https://aur.archlinux.org/rpc"
|
||||
DEFAULT_RPC_VERSION = "5"
|
||||
|
||||
def __init__(self, rpc_url: Optional[str] = None, rpc_version: Optional[str] = None) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param rpc_url: AUR RPC url
|
||||
:param rpc_version: AUR RPC version
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
parse RPC response to package list
|
||||
:param response: RPC response json
|
||||
:return: list of parsed packages
|
||||
"""
|
||||
response_type = response["type"]
|
||||
if response_type == "error":
|
||||
error_details = response.get("error", "Unknown API error")
|
||||
raise InvalidPackageInfo(error_details)
|
||||
return [AURPackage.from_json(package) for package in response["results"]]
|
||||
|
||||
def make_request(self, request_type: str, *args: str, **kwargs: str) -> List[AURPackage]:
|
||||
"""
|
||||
perform request to AUR RPC
|
||||
:param request_type: AUR request type, e.g. search, info
|
||||
:param args: list of arguments to be passed as args query parameter
|
||||
:param kwargs: list of additional named parameters like by
|
||||
:return: response parsed to package list
|
||||
"""
|
||||
query: Dict[str, Any] = {
|
||||
"type": request_type,
|
||||
"v": self.rpc_version
|
||||
}
|
||||
|
||||
arg_query = "arg[]" if len(args) > 1 else "arg"
|
||||
query[arg_query] = list(args)
|
||||
|
||||
for key, value in kwargs.items():
|
||||
query[key] = value
|
||||
|
||||
try:
|
||||
response = requests.get(self.rpc_url, params=query)
|
||||
response.raise_for_status()
|
||||
return self.parse_response(response.json())
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception(
|
||||
"could not perform request by using type %s: %s",
|
||||
request_type,
|
||||
exception_response_text(e))
|
||||
raise
|
||||
except Exception:
|
||||
self.logger.exception("could not perform request by using type %s", request_type)
|
||||
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("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]:
|
||||
"""
|
||||
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)
|
@ -17,7 +17,6 @@
|
||||
# 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 aur # type: ignore
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
@ -25,29 +24,11 @@ import requests
|
||||
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generator, Iterable, List, Optional, Union
|
||||
from typing import Any, Dict, Generator, Iterable, Optional, Union
|
||||
|
||||
from ahriman.core.exceptions import InvalidOption, UnsafeRun
|
||||
|
||||
|
||||
def aur_search(*terms: str) -> List[aur.Package]:
|
||||
"""
|
||||
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 terms: search terms, e.g. "ahriman", "is", "cool"
|
||||
:return: list of packages each of them matches all search terms
|
||||
"""
|
||||
packages: Dict[str, aur.Package] = {}
|
||||
for term in filter(lambda word: len(word) > 3, terms):
|
||||
portion = aur.search(term)
|
||||
packages = {
|
||||
package.package_base: package
|
||||
for package in portion
|
||||
if package.package_base in packages or not packages
|
||||
}
|
||||
return list(packages.values())
|
||||
|
||||
|
||||
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
|
||||
input_data: Optional[str] = None, logger: Optional[Logger] = None) -> str:
|
||||
"""
|
||||
|
109
src/ahriman/models/aur_package.py
Normal file
109
src/ahriman/models/aur_package.py
Normal file
@ -0,0 +1,109 @@
|
||||
#
|
||||
# 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 datetime
|
||||
import inflection
|
||||
|
||||
from dataclasses import dataclass, field, fields
|
||||
from typing import Any, Callable, Dict, List, Optional, Type
|
||||
|
||||
from ahriman.core.util import filter_json
|
||||
|
||||
|
||||
@dataclass
|
||||
class AURPackage:
|
||||
"""
|
||||
AUR package descriptor
|
||||
:ivar id: package ID
|
||||
:ivar name: package name
|
||||
:ivar package_base_id: package base ID
|
||||
:ivar version: package base version
|
||||
:ivar description: package base description
|
||||
:ivar url: package upstream URL
|
||||
:ivar num_votes: number of votes for the package
|
||||
:ivar polularity: package popularity
|
||||
:ivar out_of_date: package out of date timestamp if any
|
||||
:ivar maintainer: package maintainer
|
||||
:ivar first_submitted: timestamp of the first package submission
|
||||
:ivar last_modified: timestamp of the last package submission
|
||||
:ivar url_path: AUR package path
|
||||
:ivar depends: list of package dependencies
|
||||
:ivar make_depends: list of package make dependencies
|
||||
:ivar opt_depends: list of package optional dependencies
|
||||
:ivar conflicts: conflicts list for the package
|
||||
:ivar provides: list of packages which this package provides
|
||||
:ivar license: list of package licenses
|
||||
:ivar keywords: list of package keywords
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
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
|
||||
url: Optional[str] = None
|
||||
out_of_date: Optional[datetime.datetime] = None
|
||||
maintainer: Optional[str] = None
|
||||
depends: List[str] = field(default_factory=list)
|
||||
make_depends: List[str] = field(default_factory=list)
|
||||
opt_depends: List[str] = field(default_factory=list)
|
||||
conflicts: List[str] = field(default_factory=list)
|
||||
provides: List[str] = field(default_factory=list)
|
||||
license: List[str] = field(default_factory=list)
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[AURPackage], dump: Dict[str, Any]) -> AURPackage:
|
||||
"""
|
||||
construct package descriptor from RPC properties
|
||||
:param dump: json dump body
|
||||
:return: AUR package descriptor
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
properties = cls.convert(dump)
|
||||
return cls(**filter_json(properties, known_fields))
|
||||
|
||||
@staticmethod
|
||||
def convert(descriptor: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
covert AUR RPC key names to package keys
|
||||
:param descriptor: RPC package descriptor
|
||||
:return: package descriptor with names converted to snake case
|
||||
"""
|
||||
identity_mapper: Callable[[Any], Any] = lambda value: value
|
||||
value_mapper: Dict[str, Callable[[Any], Any]] = {
|
||||
"out_of_date": lambda value: datetime.datetime.utcfromtimestamp(value) if value is not None else None,
|
||||
"first_submitted": datetime.datetime.utcfromtimestamp,
|
||||
"last_modified": datetime.datetime.utcfromtimestamp,
|
||||
}
|
||||
|
||||
result: Dict[str, Any] = {}
|
||||
for api_key, api_value in descriptor.items():
|
||||
property_key = inflection.underscore(api_key)
|
||||
mapper = value_mapper.get(property_key, identity_mapper)
|
||||
result[property_key] = mapper(api_value)
|
||||
|
||||
return result
|
@ -19,7 +19,6 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import aur # type: ignore
|
||||
import copy
|
||||
import logging
|
||||
|
||||
@ -29,6 +28,7 @@ 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 ahriman.core.alpm.aur import AUR
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.exceptions import InvalidPackageInfo
|
||||
from ahriman.core.util import check_output
|
||||
@ -129,7 +129,7 @@ class Package:
|
||||
:param aur_url: AUR root url
|
||||
:return: package properties
|
||||
"""
|
||||
package = aur.info(name)
|
||||
package = AUR.info(name)
|
||||
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
|
||||
|
||||
@classmethod
|
||||
|
@ -25,8 +25,8 @@ from aiohttp import web
|
||||
from aiohttp.web import middleware, Request
|
||||
from aiohttp.web_response import StreamResponse
|
||||
from aiohttp.web_urldispatcher import StaticResource
|
||||
from aiohttp_session import setup as setup_session # type: ignore
|
||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage # type: ignore
|
||||
from aiohttp_session import setup as setup_session
|
||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||
from cryptography import fernet
|
||||
from typing import Optional
|
||||
|
||||
|
@ -17,12 +17,11 @@
|
||||
# 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 aur # type: ignore
|
||||
|
||||
from aiohttp.web import HTTPNotFound, Response, json_response
|
||||
from typing import Callable, List
|
||||
|
||||
from ahriman.core.util import aur_search
|
||||
from ahriman.core.alpm.aur import AUR
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
@ -45,11 +44,11 @@ class SearchView(BaseView):
|
||||
:return: 200 with found package bases and descriptions sorted by base
|
||||
"""
|
||||
search: List[str] = self.request.query.getall("for", default=[])
|
||||
packages = aur_search(*search)
|
||||
packages = AUR.multisearch(*search)
|
||||
if not packages:
|
||||
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
|
||||
|
||||
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
|
||||
comparator: Callable[[AURPackage], str] = lambda item: str(item.package_base)
|
||||
response = [
|
||||
{
|
||||
"package": package.package_base,
|
||||
|
Reference in New Issue
Block a user