Compare commits

...

19 Commits
1.6.0 ... 1.8.0

Author SHA1 Message Date
e414616bbd Release 1.8.0 2022-02-06 05:45:50 +03:00
60a2e25b9a update for new aiohttp api 2022-02-06 04:05:33 +03:00
683abca9e5 use own aur wrapper (#49) 2022-02-06 03:44:57 +03:00
5a3770b739 Release 1.7.0 2021-12-26 02:01:09 +03:00
52cd9a0ea9 make mypy happy 2021-12-26 01:58:55 +03:00
bfca7e41ab handle dependencies recursively 2021-12-22 19:35:09 +03:00
603c5449a8 initial implementation of the local git clones (#48) 2021-12-22 15:56:24 +03:00
5aac3db2d5 do not read aur_url from settings, use repository property instead 2021-11-15 11:27:41 +03:00
3c5bcbd172 Release 1.6.4 2021-11-10 21:29:45 +03:00
042638d40e handle packages which have been removed from the repository (#45)
* handle packages which have been removed from the repository

* manually remove packages which have been removed from the base
2021-11-10 01:37:25 +03:00
e6adb333b2 Release 1.6.3 2021-11-04 21:32:27 +03:00
fa4244d21e take python laziness into account 2021-11-04 21:30:34 +03:00
91de1c2b8a Release 1.6.2 2021-10-28 03:20:52 +03:00
32a4a82603 improve configuration extension
* Allow spaces in lists. This feature has been done in the way as shell
  interprets arguments by using quotation marks
* Clear current content on reload
2021-10-28 03:19:50 +03:00
e8a10c1bb5 add nginx configuration to the faq 2021-10-27 03:35:33 +03:00
d480eb7bc3 Release 1.6.1 2021-10-27 03:16:53 +03:00
8b0f9bfd78 update license headers 2021-10-27 03:14:39 +03:00
a2639f8dbb add update printer which will print current version if any 2021-10-27 03:11:43 +03:00
65ba590ace use PackageSource enum for Package.load method
When using add function it sill tries to load data with invalid source
2021-10-27 02:49:23 +03:00
67 changed files with 4360 additions and 3003 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 388 KiB

After

Width:  |  Height:  |  Size: 406 KiB

View File

@ -119,7 +119,7 @@ remove user
web server
.SH OPTIONS 'ahriman aur-search'
usage: ahriman aur-search [-h] [-i]
[--sort-by {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}]
[--sort-by {conflicts,depends,description,first_submitted,id,keywords,last_modified,license,maintainer,make_depends,name,num_votes,opt_depends,out_of_date,package_base,package_base_id,popularity,provides,url,url_path,version}]
search [search ...]
search for package in AUR using API
@ -133,13 +133,13 @@ search terms, can be specified multiple times, result will match all terms
show additional package information
.TP
\fB\-\-sort\-by\fR {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}
\fB\-\-sort\-by\fR {conflicts,depends,description,first_submitted,id,keywords,last_modified,license,maintainer,make_depends,name,num_votes,opt_depends,out_of_date,package_base,package_base_id,popularity,provides,url,url_path,version}
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
by name
.SH OPTIONS 'ahriman search'
usage: ahriman aur-search [-h] [-i]
[--sort-by {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}]
[--sort-by {conflicts,depends,description,first_submitted,id,keywords,last_modified,license,maintainer,make_depends,name,num_votes,opt_depends,out_of_date,package_base,package_base_id,popularity,provides,url,url_path,version}]
search [search ...]
search for package in AUR using API
@ -153,7 +153,7 @@ search terms, can be specified multiple times, result will match all terms
show additional package information
.TP
\fB\-\-sort\-by\fR {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}
\fB\-\-sort\-by\fR {conflicts,depends,description,first_submitted,id,keywords,last_modified,license,maintainer,make_depends,name,num_votes,opt_depends,out_of_date,package_base,package_base_id,popularity,provides,url,url_path,version}
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
by name
@ -496,7 +496,7 @@ create empty repository tree. Optional command for auto architecture support
.SH OPTIONS 'ahriman repo-rebuild'
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON]
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON] [--dry-run]
force rebuild whole repository
@ -505,8 +505,12 @@ force rebuild whole repository
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
only rebuild packages that depend on specified package
.TP
\fB\-\-dry\-run\fR
just perform check for packages without rebuild process itself
.SH OPTIONS 'ahriman rebuild'
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON]
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON] [--dry-run]
force rebuild whole repository
@ -515,6 +519,10 @@ force rebuild whole repository
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
only rebuild packages that depend on specified package
.TP
\fB\-\-dry\-run\fR
just perform check for packages without rebuild process itself
.SH OPTIONS 'ahriman repo-remove-unknown'
usage: ahriman repo-remove-unknown [-h] [--dry-run] [-i]
@ -695,7 +703,7 @@ target to sync
.SH OPTIONS 'ahriman repo-update'
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
check for packages updates and run build process if requested
@ -711,6 +719,10 @@ just perform check for updates, same as check command
\fB\-\-no\-aur\fR
do not check for AUR updates. Implies \-\-no\-vcs
.TP
\fB\-\-no\-local\fR
do not check local packages for updates
.TP
\fB\-\-no\-manual\fR
do not include manual updates
@ -720,7 +732,7 @@ do not include manual updates
do not check VCS packages
.SH OPTIONS 'ahriman update'
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
check for packages updates and run build process if requested
@ -736,6 +748,10 @@ just perform check for updates, same as check command
\fB\-\-no\-aur\fR
do not check for AUR updates. Implies \-\-no\-vcs
.TP
\fB\-\-no\-local\fR
do not check local packages for updates
.TP
\fB\-\-no\-manual\fR
do not include manual updates

View File

@ -1,6 +1,13 @@
# ahriman configuration
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
Some values have list of strings type. Those values will be read in the same way as shell does:
* By default, it splits value by spaces excluding empty elements.
* In case if quotation mark (`"` or `'`) will be found, any spaces inside will be ignored.
* In order to use quotation mark inside value it is required to put it to another quotation mark, e.g. `wor"'"d "with quote"` will be parsed as `["wor'd", "with quote"]` and vice versa.
* Unclosed quotation mark is not allowed and will rise an exception.
## `settings` group
@ -38,11 +45,11 @@ Authorization mapping. Group name must refer to user access level, i.e. it shoul
Key is always username (case-insensitive), option value depends on authorization provider:
* `OAuth` - by default requires only usernames and ignores values. But in case of direct login method call (via POST request) it will act as `Mapping` authorization method.
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `create-user` subcommand.
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `user-add` subcommand.
## `build:*` groups
Build related configuration. Group name must refer to architecture, e.g. it should be `build:x86_64` for x86_64 architecture.
Build related configuration. Group name can refer to architecture, e.g. `build:x86_64` can be used for x86_64 architecture specific settings.
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
* `build_command` - default build command, string, required.
@ -59,7 +66,7 @@ Base repository settings.
## `sign:*` groups
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign:x86_64` for x86_64 architecture.
Settings for signing packages or repository. Group name can refer to architecture, e.g. `sign:x86_64` can be used for x86_64 architecture specific settings.
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
@ -69,7 +76,7 @@ Settings for signing packages or repository. Group name must refer to architectu
Report generation settings.
* `target` - list of reports to be generated, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `email` must point to one of `email` of `email:x86_64` (with architecture it has higher priority).
* `target` - list of reports to be generated, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `email` must point to one of `email` of `email:x86_64` (the one with architecture has higher priority).
Type will be read from several ways:
@ -152,7 +159,7 @@ Requires `boto3` library to be installed. Section name must be either `s3` (plus
## `web:*` groups
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture. This feature requires `aiohttp` libraries to be installed.
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name can refer to architecture, e.g. `web:x86_64` can be used for x86_64 architecture specific settings. This feature requires `aiohttp` libraries to be installed.
* `address` - optional address in form `proto://host:port` (`port` can be omitted in case of default `proto` ports), will be used instead of `http://{host}:{port}` in case if set, string, optional. This option is required in case if `OAuth` provider is used.
* `debug` - enable debug toolbar, boolean, optional, default `no`.

View File

@ -163,6 +163,41 @@ Server = file:///var/lib/ahriman/repository/x86_64
(You might need to add `SigLevel` option according to the pacman documentation.)
### I would like to serve the repository
Easy. For example, nginx configuration (without SSL) will look like:
```
server {
listen 80;
server_name repo.example.com;
location / {
autoindex on;
root /var/lib/ahriman/repository;
}
}
```
Example of the status page configuration is the following (status service is using 8080 port):
```
server {
listen 80;
server_name builds.example.com;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarder-Proto $scheme;
proxy_pass http://127.0.0.1:8080;
}
}
```
## Remote synchronization
### Wait I would like to use the repository from another server

View File

@ -1,13 +1,13 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=1.6.0
pkgver=1.8.0
pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager"
arch=('any')
url="https://github.com/arcan1s/ahriman"
license=('GPL3')
depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-passlib' 'python-srcinfo')
depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-srcinfo')
makedepends=('python-pip')
optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support'

View File

@ -3,3 +3,4 @@ test = pytest
[tool:pytest]
addopts = --cov=ahriman --cov-report term-missing:skip-covered --pspec
asyncio_mode = auto

View File

@ -29,7 +29,7 @@ setup(
dependency_links=[
],
install_requires=[
"aur",
"inflection",
"passlib",
"pyalpm",
"requests",

View File

@ -22,6 +22,7 @@ import sys
import tempfile
from pathlib import Path
from typing import TypeVar
from ahriman import version
from ahriman.application import handlers
@ -32,8 +33,11 @@ from ahriman.models.sign_settings import SignSettings
from ahriman.models.user_access import UserAccess
# pylint thinks it is bad idea, but get the fuck off
SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access
# this workaround is for several things
# firstly python devs don't think that is it error and asking you for workarounds https://bugs.python.org/issue41592
# secondly linters don't like when you are importing private members
# thirdly new mypy doesn't like _SubParsersAction and thinks it is a template
SubParserAction = TypeVar("SubParserAction", bound="argparse._SubParsersAction[argparse.ArgumentParser]")
def _formatter(prog: str) -> argparse.HelpFormatter:
@ -285,7 +289,7 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=_formatter)
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, dry_run=True, no_aur=False, no_manual=True)
parser.set_defaults(handler=handlers.Update, dry_run=True, no_aur=False, no_local=False, no_manual=True)
return parser
@ -346,6 +350,8 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository",
description="force rebuild whole repository", formatter_class=_formatter)
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
parser.add_argument("--dry-run", help="just perform check for packages without rebuild process itself",
action="store_true")
parser.set_defaults(handler=handlers.Rebuild)
return parser
@ -461,6 +467,7 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
parser.add_argument("--no-local", help="do not check local packages for updates", action="store_true")
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update)

View File

@ -64,12 +64,10 @@ class Packages(Properties):
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
"""
aur_url = self.configuration.get("alpm", "aur_url")
package = Package.load(source, self.repository.pacman, aur_url)
Sources.load(self.repository.paths.manual_for(package.base), package.git_url,
self.repository.paths.patches_for(package.base))
package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url)
local_path = self.repository.paths.manual_for(package.base)
Sources.load(local_path, package.git_url, self.repository.paths.patches_for(package.base))
self._process_dependencies(local_path, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None:
@ -88,11 +86,9 @@ class Packages(Properties):
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
"""
local_path = Path(source)
aur_url = self.configuration.get("alpm", "aur_url")
package = Package.load(local_path, self.repository.pacman, aur_url)
package = Package.load(source, PackageSource.Local, self.repository.pacman, self.repository.aur_url)
cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(local_path, cache_dir) # copy package to store in caches
shutil.copytree(Path(source), cache_dir) # copy package to store in caches
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
dst = self.repository.paths.manual_for(package.base)

View File

@ -23,9 +23,11 @@ from pathlib import Path
from typing import Callable, Iterable, List
from ahriman.application.application.properties import Properties
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.core.build_tools.sources import Sources
from ahriman.core.tree import Tree
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Repository(Properties):
@ -103,27 +105,37 @@ class Repository(Properties):
targets = target or None
self.repository.process_sync(targets, built_packages)
def unknown(self) -> List[Package]:
def unknown(self) -> List[str]:
"""
get packages which were not found in AUR
:return: unknown package list
:return: unknown package archive list
"""
def has_aur(package_base: str, aur_url: str) -> bool:
try:
_ = Package.from_aur(package_base, aur_url)
except Exception:
return False
return True
def has_local(package_base: str) -> bool:
cache_dir = self.repository.paths.cache_for(package_base)
def has_local(probe: Package) -> bool:
cache_dir = self.repository.paths.cache_for(probe.base)
return cache_dir.is_dir() and not Sources.has_remotes(cache_dir)
return [
package
for package in self.repository.packages()
if not has_aur(package.base, package.aur_url) and not has_local(package.base)
]
def unknown_aur(probe: Package) -> List[str]:
packages: List[str] = []
for single in probe.packages:
try:
_ = Package.from_aur(single, probe.aur_url)
except Exception:
packages.append(single)
return packages
def unknown_local(probe: Package) -> List[str]:
cache_dir = self.repository.paths.cache_for(probe.base)
local = Package.from_build(cache_dir, probe.aur_url)
packages = set(probe.packages.keys()).difference(local.packages.keys())
return list(packages)
result = []
for package in self.repository.packages():
if has_local(package):
result.extend(unknown_local(package)) # there is local package
else:
result.extend(unknown_aur(package)) # local package not found
return result
def update(self, updates: Iterable[Package]) -> None:
"""
@ -133,7 +145,10 @@ class Repository(Properties):
def process_update(paths: Iterable[Path]) -> None:
if not paths:
return # don't need to process if no update supplied
updated = [Package.load(path, self.repository.pacman, self.repository.aur_url) for path in paths]
updated = [
Package.load(str(path), PackageSource.Archive, self.repository.pacman, self.repository.aur_url)
for path in paths
]
self.repository.process_update(paths)
self._finalize(updated)
@ -148,25 +163,31 @@ class Repository(Properties):
packages = self.repository.process_build(level)
process_update(packages)
def updates(self, filter_packages: Iterable[str], no_aur: bool, no_manual: bool, no_vcs: bool,
def updates(self, filter_packages: Iterable[str], no_aur: bool, no_local: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]:
"""
get list of packages to run update process
:param filter_packages: do not check every package just specified in the list
:param no_aur: do not check for aur updates
:param no_local: do not check local packages for updates
:param no_manual: do not check for manual updates
:param no_vcs: do not check VCS packages
:param log_fn: logger function to log updates
:return: list of out-of-dated packages
"""
updates = []
updates = {}
if not no_aur:
updates.extend(self.repository.updates_aur(filter_packages, no_vcs))
updates.update({package.base: package for package in self.repository.updates_aur(filter_packages, no_vcs)})
if not no_local:
updates.update({package.base: package for package in self.repository.updates_local()})
if not no_manual:
updates.extend(self.repository.updates_manual())
updates.update({package.base: package for package in self.repository.updates_manual()})
for package in updates:
log_fn(f"{package.base} = {package.version}")
local_versions = {package.base: package.version for package in self.repository.packages()}
updated_packages = [package for _, package in sorted(updates.items())]
for package in updated_packages:
UpdatePrinter(package, local_versions.get(package.base)).print(
verbose=True, log_fn=log_fn, separator=" -> ")
return updates
return updated_packages

View File

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

View File

@ -17,11 +17,10 @@
# 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 typing import List, Optional
from typing import Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.build_status import BuildStatus
from ahriman.models.property import Property
class StatusPrinter(Printer):
@ -36,13 +35,6 @@ class StatusPrinter(Printer):
"""
self.content = status
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return []
def title(self) -> Optional[str]:
"""
generate entry title from content

View File

@ -0,0 +1,42 @@
#
# Copyright (c) 2021 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 typing import Optional
from ahriman.application.formatters.printer import Printer
class StringPrinter(Printer):
"""
print content of the random string
"""
def __init__(self, content: str) -> None:
"""
default constructor
:param content: any content string
"""
self.content = content
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content

View File

@ -0,0 +1,53 @@
#
# Copyright (c) 2021 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 typing import List, Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.package import Package
from ahriman.models.property import Property
class UpdatePrinter(Printer):
"""
print content of the package update
"""
def __init__(self, remote: Package, local_version: Optional[str]) -> None:
"""
default constructor
:param remote: remote (new) package object
:param local_version: local version of the package if any
"""
self.content = remote
self.local_version = local_version or "N/A"
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [Property(self.local_version, self.content.version, is_required=True)]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return self.content.base

View File

@ -46,5 +46,5 @@ class Add(Handler):
if not args.now:
return
packages = application.updates(args.package, True, False, True, application.logger.info)
packages = application.updates(args.package, True, True, False, True, application.logger.info)
application.update(packages)

View File

@ -29,6 +29,7 @@ from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Patch(Handler):
@ -55,23 +56,24 @@ class Patch(Handler):
elif args.action == Action.Remove:
Patch.patch_set_remove(application, args.package)
elif args.action == Action.Update:
Patch.patch_set_create(application, Path(args.package), args.track)
Patch.patch_set_create(application, args.package, args.track)
@staticmethod
def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None:
def patch_set_create(application: Application, sources_dir: str, track: List[str]) -> None:
"""
create patch set for the package base
:param application: application instance
:param sources_dir: path to directory with the package sources
:param track: track files which match the glob before creating the patch
"""
package = Package.load(sources_dir, application.repository.pacman, application.repository.aur_url)
package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman,
application.repository.aur_url)
patch_dir = application.repository.paths.patches_for(package.base)
Patch.patch_set_remove(application, package.base) # remove old patches
patch_dir.mkdir(mode=0o755, parents=True)
Sources.patch_create(sources_dir, patch_dir / "00-main.patch", *track)
Sources.patch_create(Path(sources_dir), patch_dir / "00-main.patch", *track)
@staticmethod
def patch_set_list(application: Application, package_base: str) -> None:

View File

@ -22,6 +22,7 @@ import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
@ -44,9 +45,10 @@ class Rebuild(Handler):
depends_on = set(args.depends_on) if args.depends_on else None
application = Application(architecture, configuration, no_report)
packages = [
package
for package in application.repository.packages()
if depends_on is None or depends_on.intersection(package.depends)
] # we have to use explicit list here for testing purpose
application.update(packages)
updates = application.repository.packages_depends_on(depends_on)
if args.dry_run:
for package in updates:
UpdatePrinter(package, package.version).print(verbose=True)
return
application.update(updates)

View File

@ -22,10 +22,9 @@ import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.string_printer import StringPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
class RemoveUnknown(Handler):
@ -46,8 +45,8 @@ class RemoveUnknown(Handler):
application = Application(architecture, configuration, no_report)
unknown_packages = application.unknown()
if args.dry_run:
for package in unknown_packages:
PackagePrinter(package, BuildStatus()).print(args.info)
for package in sorted(unknown_packages):
StringPrinter(package).print(args.info)
return
application.remove(package.base for package in unknown_packages)
application.remove(unknown_packages)

View File

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

View File

@ -42,7 +42,7 @@ class Update(Handler):
:param no_report: force disable reporting
"""
application = Application(architecture, configuration, no_report)
packages = application.updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
packages = application.updates(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
if args.dry_run:
return

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

View File

@ -20,7 +20,7 @@
import logging
from pathlib import Path
from typing import List
from typing import List, Optional
from ahriman.core.util import check_output
@ -64,7 +64,7 @@ class Sources:
patch_path.write_text(patch)
@staticmethod
def fetch(sources_dir: Path, remote: str) -> None:
def fetch(sources_dir: Path, remote: Optional[str]) -> None:
"""
either clone repository or update it to origin/`branch`
:param sources_dir: local path to fetch
@ -81,6 +81,8 @@ class Sources:
Sources.logger.info("update HEAD to remote at %s", sources_dir)
Sources._check_output("git", "fetch", "origin", Sources._branch,
exception=None, cwd=sources_dir, logger=Sources.logger)
elif remote is None:
Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir)
else:
Sources.logger.info("clone remote %s to %s", remote, sources_dir)
Sources._check_output("git", "clone", remote, str(sources_dir), exception=None, logger=Sources.logger)

View File

@ -24,7 +24,7 @@ import logging
from logging.config import fileConfig
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeException
@ -49,7 +49,7 @@ class Configuration(configparser.RawConfigParser):
default constructor. In the most cases must not be called directly
"""
configparser.RawConfigParser.__init__(self, allow_no_value=True, converters={
"list": lambda value: value.split(),
"list": self.__convert_list,
"path": self.__convert_path,
})
self.architecture: Optional[str] = None
@ -84,6 +84,32 @@ class Configuration(configparser.RawConfigParser):
config.load_logging(quiet)
return config
@staticmethod
def __convert_list(value: str) -> List[str]:
"""
convert string value to list of strings
:param value: string configuration value
:return: list of string from the parsed string
"""
def generator() -> Generator[str, None, None]:
quote_mark = None
word = ""
for char in value:
if char in ("'", "\"") and quote_mark is None: # quoted part started, store quote and do nothing
quote_mark = char
elif char == quote_mark: # quoted part ended, reset quotation
quote_mark = None
elif char == " " and quote_mark is None: # found space outside of the quotation, yield the word
yield word
word = ""
else: # append character to the buffer
word += char
if quote_mark: # there is unmatched quote
raise ValueError(f"unmatched quote in {value}")
yield word # sequence done, return whatever we found
return [word for word in generator() if word]
@staticmethod
def section_name(section: str, suffix: str) -> str:
"""
@ -204,6 +230,8 @@ class Configuration(configparser.RawConfigParser):
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
for section in self.sections(): # clear current content
self.remove_section(section)
self.load(self.path)
self.merge_sections(self.architecture)

View File

@ -153,8 +153,12 @@ class UnknownPackage(ValueError):
exception for status watcher which will be thrown on unknown package
"""
def __init__(self, base: str) -> None:
ValueError.__init__(self, f"Package base {base} is unknown")
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package_base: package base name
"""
ValueError.__init__(self, f"Package base {package_base} is unknown")
class UnsafeRun(RuntimeError):
@ -165,9 +169,9 @@ class UnsafeRun(RuntimeError):
def __init__(self, current_uid: int, root_uid: int) -> None:
"""
default constructor
:param current_uid: current user ID
:param root_uid: ID of the owner of root directory
"""
RuntimeError.__init__(
self,
f"""Current UID {current_uid} differs from root owner {root_uid}.
Note that for the most actions it is unsafe to run application as different user.
If you are 100% sure that it must be there try --unsafe option""")
RuntimeError.__init__(self, f"Current UID {current_uid} differs from root owner {root_uid}. "
f"Note that for the most actions it is unsafe to run application as different user."
f" If you are 100% sure that it must be there try --unsafe option")

View File

@ -20,7 +20,7 @@
import shutil
from pathlib import Path
from typing import Dict, Iterable, List, Optional
from typing import Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task
from ahriman.core.report.report import Report
@ -34,6 +34,14 @@ class Executor(Cleaner):
trait for common repository update processes
"""
def load_archives(self, packages: Iterable[Path]) -> List[Package]:
"""
load packages from list of archives
:param packages: paths to package archives
:return: list of read packages
"""
raise NotImplementedError
def packages(self) -> List[Package]:
"""
generate list of repository packages
@ -151,23 +159,24 @@ class Executor(Cleaner):
package_path = self.paths.repository / name
self.repo.add(package_path)
# we are iterating over bases, not single packages
updates: Dict[str, Package] = {}
for filename in packages:
try:
local = Package.load(filename, self.pacman, self.aur_url)
updates.setdefault(local.base, local).packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", filename)
current_packages = self.packages()
removed_packages: List[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages)
for local in updates.values():
for local in updates:
try:
for description in local.packages.values():
update_single(description.filename, local.base)
self.reporter.set_success(local)
current_package_archives: Set[str] = next(
(set(current.packages) for current in current_packages if current.base == local.base), set())
removed_packages.extend(current_package_archives.difference(local.packages))
except Exception:
self.reporter.set_failed(local.base)
self.logger.exception("could not process %s", local.base)
self.clear_packages()
self.process_remove(removed_packages)
return self.repo.repo_path

View File

@ -18,12 +18,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pathlib import Path
from typing import Dict, List
from typing import Dict, Iterable, List, Optional
from ahriman.core.repository.executor import Executor
from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.core.util import package_like
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Repository(Executor, UpdateHandler):
@ -31,20 +32,35 @@ class Repository(Executor, UpdateHandler):
base repository control class
"""
def load_archives(self, packages: Iterable[Path]) -> List[Package]:
"""
load packages from list of archives
:param packages: paths to package archives
:return: list of read packages
"""
result: Dict[str, Package] = {}
# we are iterating over bases, not single packages
for full_path in packages:
try:
local = Package.load(str(full_path), PackageSource.Archive, self.pacman, self.aur_url)
current = result.setdefault(local.base, local)
if current.version != local.version:
# force version to max of them
self.logger.warning("version of %s differs, found %s and %s",
current.base, current.version, local.version)
if current.is_outdated(local, self.paths, calculate_version=False):
current.version = local.version
current.packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", full_path)
return list(result.values())
def packages(self) -> List[Package]:
"""
generate list of repository packages
:return: list of packages properties
"""
result: Dict[str, Package] = {}
for full_path in filter(package_like, self.paths.repository.iterdir()):
try:
local = Package.load(full_path, self.pacman, self.aur_url)
result.setdefault(local.base, local).packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", full_path)
continue
return list(result.values())
return self.load_archives(filter(package_like, self.paths.repository.iterdir()))
def packages_built(self) -> List[Path]:
"""
@ -52,3 +68,20 @@ class Repository(Executor, UpdateHandler):
:return: list of filenames from the directory
"""
return list(filter(package_like, self.paths.packages.iterdir()))
def packages_depends_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
"""
extract list of packages which depends on specified package
:param: depends_on: dependencies of the packages
:return: list of repository packages which depend on specified packages
"""
packages = self.packages()
if depends_on is None:
return packages # no list provided extract everything by default
depends_on = set(depends_on)
return [
package
for package in packages
if depends_on is None or depends_on.intersection(package.full_depends(self.pacman, packages))
]

View File

@ -19,8 +19,10 @@
#
from typing import Iterable, List
from ahriman.core.build_tools.sources import Sources
from ahriman.core.repository.cleaner import Cleaner
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class UpdateHandler(Cleaner):
@ -53,7 +55,7 @@ class UpdateHandler(Cleaner):
continue
try:
remote = Package.load(local.base, self.pacman, self.aur_url)
remote = Package.load(local.base, PackageSource.AUR, self.pacman, self.aur_url)
if local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base)
result.append(remote)
@ -64,6 +66,31 @@ class UpdateHandler(Cleaner):
return result
def updates_local(self) -> List[Package]:
"""
check local packages for updates
:return: list of local packages which are out-of-dated
"""
result: List[Package] = []
packages = {local.base: local for local in self.packages()}
for dirname in self.paths.cache.iterdir():
try:
Sources.fetch(dirname, remote=None)
remote = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url)
local = packages.get(remote.base)
if local is None:
self.reporter.set_unknown(remote)
result.append(remote)
elif local.is_outdated(remote, self.paths):
self.reporter.set_pending(local.base)
result.append(remote)
except Exception:
self.logger.exception("could not procees package at %s", dirname)
return result
def updates_manual(self) -> List[Package]:
"""
check for packages for which manual update has been requested
@ -72,16 +99,16 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
known_bases = {package.base for package in self.packages()}
for filename in self.paths.manual.iterdir():
for dirname in self.paths.manual.iterdir():
try:
local = Package.load(filename, self.pacman, self.aur_url)
local = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url)
result.append(local)
if local.base not in known_bases:
self.reporter.set_unknown(local)
else:
self.reporter.set_pending(local.base)
except Exception:
self.logger.exception("could not add package from %s", filename)
self.logger.exception("could not add package from %s", dirname)
self.clear_manual()
return result

View File

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

View 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

View File

@ -21,7 +21,7 @@ from __future__ import annotations
import datetime
from dataclasses import dataclass, fields
from dataclasses import dataclass, field, fields
from enum import Enum
from typing import Any, Dict, Type
@ -84,7 +84,7 @@ class BuildStatus:
"""
status: BuildStatusEnum = BuildStatusEnum.Unknown
timestamp: int = int(datetime.datetime.utcnow().timestamp())
timestamp: int = field(default_factory=lambda: int(datetime.datetime.utcnow().timestamp()))
def __post_init__(self) -> None:
"""

View File

@ -19,19 +19,21 @@
#
from __future__ import annotations
import aur # type: ignore
import copy
import logging
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, List, Optional, Set, Type, Union
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
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@ -127,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
@ -164,21 +166,24 @@ class Package:
packages=packages)
@classmethod
def load(cls: Type[Package], path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package:
"""
package constructor from available sources
:param path: one of path to sources directory, path to archive or package name/base
:param package: one of path to sources directory, path to archive or package name/base
:param source: source of the package required to define the load method
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
"""
try:
maybe_path = Path(path)
if maybe_path.is_dir():
return cls.from_build(maybe_path, aur_url)
if maybe_path.is_file():
return cls.from_archive(maybe_path, pacman, aur_url)
return cls.from_aur(str(path), aur_url)
resolved_source = source.resolve(package)
if resolved_source == PackageSource.Archive:
return cls.from_archive(Path(package), pacman, aur_url)
if resolved_source == PackageSource.AUR:
return cls.from_aur(package, aur_url)
if resolved_source == PackageSource.Local:
return cls.from_build(Path(package), aur_url)
raise InvalidPackageInfo(f"Unsupported local package source {resolved_source}")
except InvalidPackageInfo:
raise
except Exception as e:
@ -253,14 +258,45 @@ class Package:
return self.version
def is_outdated(self, remote: Package, paths: RepositoryPaths) -> bool:
def full_depends(self, pacman: Pacman, packages: Iterable[Package]) -> List[str]:
"""
generate full dependencies list including transitive dependencies
:param pacman: alpm wrapper instance
:param packages: repository package list
:return: all dependencies of the package
"""
dependencies = {}
# load own package dependencies
for package_base in packages:
for name, repo_package in package_base.packages.items():
dependencies[name] = repo_package.depends
for provides in repo_package.provides:
dependencies[provides] = repo_package.depends
# load repository dependencies
for database in pacman.handle.get_syncdbs():
for pacman_package in database.pkgcache:
dependencies[pacman_package.name] = pacman_package.depends
for provides in pacman_package.provides:
dependencies[provides] = pacman_package.depends
result = set(self.depends)
current_depends: Set[str] = set()
while result != current_depends:
current_depends = copy.deepcopy(result)
for package in current_depends:
result.update(dependencies.get(package, []))
return sorted(result)
def is_outdated(self, remote: Package, paths: RepositoryPaths, calculate_version: bool = True) -> bool:
"""
check if package is out-of-dated
:param remote: package properties from remote source
:param paths: repository paths instance. Required for VCS packages cache
:param calculate_version: expand version to actual value (by calculating git versions)
:return: True if the package is out-of-dated and False otherwise
"""
remote_version = remote.actual_version(paths) # either normal version or updated VCS
remote_version = remote.actual_version(paths) if calculate_version else remote.version
result: int = vercmp(self.version, remote_version)
return result < 0

View File

@ -1,3 +1,7 @@
#
# Copyright (c) 2021 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

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "1.6.0"
__version__ = "1.8.0"

View File

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

View File

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

View File

@ -147,6 +147,7 @@ def test_unknown_no_aur(application_repository: Repository, package_ahriman: Pac
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception())
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=False)
@ -163,7 +164,7 @@ def test_unknown_no_aur_no_local(application_repository: Repository, package_ahr
mocker.patch("pathlib.Path.is_dir", return_value=False)
packages = application_repository.unknown()
assert packages == [package_ahriman]
assert packages == list(package_ahriman.packages.keys())
def test_unknown_no_local(application_repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -201,12 +202,15 @@ def test_updates_all(application_repository: Repository, package_ahriman: Packag
"""
must get updates for all
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur",
return_value=[package_ahriman])
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application_repository.updates([], no_aur=False, no_manual=False, no_vcs=False, log_fn=print)
application_repository.updates([], no_aur=False, no_local=False, no_manual=False, no_vcs=False, log_fn=print)
updates_aur_mock.assert_called_once_with([], False)
updates_local_mock.assert_called_once()
updates_manual_mock.assert_called_once()
@ -214,11 +218,14 @@ def test_updates_disabled(application_repository: Repository, mocker: MockerFixt
"""
must get updates without anything
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application_repository.updates([], no_aur=True, no_manual=True, no_vcs=False, log_fn=print)
application_repository.updates([], no_aur=True, no_local=True, no_manual=True, no_vcs=False, log_fn=print)
updates_aur_mock.assert_not_called()
updates_local_mock.assert_not_called()
updates_manual_mock.assert_not_called()
@ -226,11 +233,29 @@ def test_updates_no_aur(application_repository: Repository, mocker: MockerFixtur
"""
must get updates without aur
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application_repository.updates([], no_aur=True, no_manual=False, no_vcs=False, log_fn=print)
application_repository.updates([], no_aur=True, no_local=False, no_manual=False, no_vcs=False, log_fn=print)
updates_aur_mock.assert_not_called()
updates_local_mock.assert_called_once()
updates_manual_mock.assert_called_once()
def test_updates_no_local(application_repository: Repository, mocker: MockerFixture) -> None:
"""
must get updates without local packages
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application_repository.updates([], no_aur=False, no_local=True, no_manual=False, no_vcs=False, log_fn=print)
updates_aur_mock.assert_called_once_with([], False)
updates_local_mock.assert_not_called()
updates_manual_mock.assert_called_once()
@ -238,11 +263,14 @@ def test_updates_no_manual(application_repository: Repository, mocker: MockerFix
"""
must get updates without manual
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application_repository.updates([], no_aur=False, no_manual=True, no_vcs=False, log_fn=print)
application_repository.updates([], no_aur=False, no_local=False, no_manual=True, no_vcs=False, log_fn=print)
updates_aur_mock.assert_called_once_with([], False)
updates_local_mock.assert_called_once()
updates_manual_mock.assert_not_called()
@ -250,21 +278,28 @@ def test_updates_no_vcs(application_repository: Repository, mocker: MockerFixtur
"""
must get updates without VCS
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application_repository.updates([], no_aur=False, no_manual=False, no_vcs=True, log_fn=print)
application_repository.updates([], no_aur=False, no_local=False, no_manual=False, no_vcs=True, log_fn=print)
updates_aur_mock.assert_called_once_with([], True)
updates_local_mock.assert_called_once()
updates_manual_mock.assert_called_once()
def test_updates_with_filter(application_repository: Repository, mocker: MockerFixture) -> None:
"""
must get updates without VCS
must get updates with filter
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[])
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
application_repository.updates(["filter"], no_aur=False, no_manual=False, no_vcs=False, log_fn=print)
application_repository.updates(["filter"], no_aur=False, no_local=False, no_manual=False, no_vcs=False,
log_fn=print)
updates_aur_mock.assert_called_once_with(["filter"], False)
updates_local_mock.assert_called_once()
updates_manual_mock.assert_called_once()

View File

@ -1,16 +1,18 @@
import aur
import pytest
from ahriman.application.formatters.aur_printer import AurPrinter
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.status_printer import StatusPrinter
from ahriman.application.formatters.string_printer import StringPrinter
from ahriman.application.formatters.update_printer import UpdatePrinter
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@pytest.fixture
def aur_package_ahriman_printer(aur_package_ahriman: aur.Package) -> AurPrinter:
def aur_package_ahriman_printer(aur_package_ahriman: AURPackage) -> AurPrinter:
"""
fixture for AUR package printer
:param aur_package_ahriman: AUR package fixture
@ -39,9 +41,27 @@ def package_ahriman_printer(package_ahriman: Package) -> PackagePrinter:
@pytest.fixture
def status_printer(package_ahriman: Package) -> StatusPrinter:
def status_printer() -> StatusPrinter:
"""
fixture for build status printer
:return: build status printer test instance
"""
return StatusPrinter(BuildStatus())
@pytest.fixture
def string_printer() -> StringPrinter:
"""
fixture for any string printer
:return: any string printer test instance
"""
return StringPrinter("hello, world")
@pytest.fixture
def update_printer(package_ahriman: Package) -> UpdatePrinter:
"""
fixture for build status printer
:return: build status printer test instance
"""
return UpdatePrinter(package_ahriman, None)

View File

@ -0,0 +1,15 @@
from ahriman.application.formatters.string_printer import StringPrinter
def test_properties(string_printer: StringPrinter) -> None:
"""
must return empty properties list
"""
assert not string_printer.properties()
def test_title(string_printer: StringPrinter) -> None:
"""
must return non empty title
"""
assert string_printer.title() is not None

View File

@ -0,0 +1,15 @@
from ahriman.application.formatters.update_printer import UpdatePrinter
def test_properties(update_printer: UpdatePrinter) -> None:
"""
must return empty properties list
"""
assert update_printer.properties()
def test_title(update_printer: UpdatePrinter) -> None:
"""
must return non empty title
"""
assert update_printer.title() is not None

View File

@ -14,6 +14,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
:return: generated arguments for these test cases
"""
args.depends_on = []
args.dry_run = False
return args
@ -23,7 +24,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
"""
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages")
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on")
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration, True)
@ -31,34 +32,43 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock.assert_called_once()
def test_run_filter(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
def test_run_dry_run(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run command with depends filter
must run command without update itself
"""
args = _default_args(args)
args.depends_on = ["python-aur"]
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
args.dry_run = True
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on", return_value=[package_ahriman])
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration, True)
application_mock.assert_called_once_with([package_ahriman])
application_mock.assert_not_called()
def test_run_without_filter(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
def test_run_filter(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with depends on filter
"""
args = _default_args(args)
args.depends_on = ["python-aur"]
mocker.patch("ahriman.application.application.Application.update")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on")
Rebuild.run(args, "x86_64", configuration, True)
application_packages_mock.assert_called_once_with({"python-aur"})
def test_run_without_filter(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command for all packages if no filter supplied
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
mocker.patch("ahriman.application.application.Application.update")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.application.Application.update")
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depends_on")
Rebuild.run(args, "x86_64", configuration, True)
application_mock.assert_called_once_with([package_ahriman, package_python_schedule])
application_packages_mock.assert_called_once_with(None)

View File

@ -1,5 +1,5 @@
import argparse
import aur
import dataclasses
import pytest
from pytest_mock import MockerFixture
@ -7,6 +7,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Search
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidOption
from ahriman.models.aur_package import AURPackage
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -21,13 +22,13 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
return args
def test_run(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
def test_run(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: AURPackage,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
search_mock = mocker.patch("ahriman.application.handlers.search.aur_search", return_value=[aur_package_ahriman])
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
Search.run(args, "x86_64", configuration, True)
@ -35,38 +36,38 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package
print_mock.assert_called_once()
def test_run_sort(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
def test_run_sort(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: AURPackage,
mocker: MockerFixture) -> None:
"""
must run command with sorting
"""
args = _default_args(args)
mocker.patch("ahriman.application.handlers.search.aur_search", return_value=[aur_package_ahriman])
mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
Search.run(args, "x86_64", configuration, True)
sort_mock.assert_called_once_with([aur_package_ahriman], "name")
def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: AURPackage,
mocker: MockerFixture) -> None:
"""
must run command with sorting by specified field
"""
args = _default_args(args)
args.sort_by = "field"
mocker.patch("ahriman.application.handlers.search.aur_search", return_value=[aur_package_ahriman])
mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman])
sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
Search.run(args, "x86_64", configuration, True)
sort_mock.assert_called_once_with([aur_package_ahriman], "field")
def test_sort(aur_package_ahriman: aur.Package) -> None:
def test_sort(aur_package_ahriman: AURPackage) -> None:
"""
must sort package list
"""
another = aur_package_ahriman._replace(name="1", package_base="base")
another = dataclasses.replace(aur_package_ahriman, name="1", package_base="base")
# sort by name
assert Search.sort([aur_package_ahriman, another], "name") == [another, aur_package_ahriman]
# sort by another field
@ -75,7 +76,7 @@ def test_sort(aur_package_ahriman: aur.Package) -> None:
assert Search.sort([aur_package_ahriman, another], "version") == [another, aur_package_ahriman]
def test_sort_exception(aur_package_ahriman: aur.Package) -> None:
def test_sort_exception(aur_package_ahriman: AURPackage) -> None:
"""
must raise an exception on unknown sorting field
"""
@ -94,4 +95,5 @@ def test_sort_fields() -> None:
"""
must store valid field list which are allowed to be used for sorting
"""
assert all(field in aur.Package._fields for field in Search.SORT_FIELDS)
expected = {pair.name for pair in dataclasses.fields(AURPackage)}
assert all(field in expected for field in Search.SORT_FIELDS)

View File

@ -16,6 +16,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.package = []
args.dry_run = False
args.no_aur = False
args.no_local = False
args.no_manual = False
args.no_vcs = False
return args

View File

@ -1,4 +1,4 @@
import aur
import datetime
import pytest
from pathlib import Path
@ -10,6 +10,7 @@ from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.aur_package import AURPackage
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths
@ -48,28 +49,56 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
# generic fixtures
@pytest.fixture
def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
def aur_package_ahriman() -> AURPackage:
"""
fixture for AUR package
:param package_ahriman: package fixture
:return: AUR package test instance
"""
return aur.Package(
num_votes=None,
description=package_ahriman.packages[package_ahriman.base].description,
url_path=package_ahriman.web_url,
last_modified=None,
name=package_ahriman.base,
return AURPackage(
id=1009791,
name="ahriman",
package_base_id=165427,
package_base="ahriman",
version="1.7.0-1",
description="ArcH Linux ReposItory MANager",
num_votes=0,
popularity=0,
first_submitted=datetime.datetime(2021, 4, 9, 22, 44, 45),
last_modified=datetime.datetime(2021, 12, 25, 23, 11, 11),
url_path="/cgit/aur.git/snapshot/ahriman.tar.gz",
url="https://github.com/arcan1s/ahriman",
out_of_date=None,
id=None,
first_submitted=None,
maintainer=None,
version=package_ahriman.version,
license=package_ahriman.packages[package_ahriman.base].licenses,
url=None,
package_base=package_ahriman.base,
package_base_id=None,
category_id=None)
maintainer="arcanis",
depends=[
"devtools",
"git",
"pyalpm",
"python-aur",
"python-passlib",
"python-srcinfo",
],
make_depends=["python-pip"],
opt_depends=[
"breezy",
"darcs",
"mercurial",
"python-aioauth-client",
"python-aiohttp",
"python-aiohttp-debugtoolbar",
"python-aiohttp-jinja2",
"python-aiohttp-security",
"python-aiohttp-session",
"python-boto3",
"python-cryptography",
"python-jinja",
"rsync",
"subversion",
],
conflicts=[],
provides=[],
license=["GPL3"],
keywords=[],
)
@pytest.fixture
@ -103,7 +132,7 @@ def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
packages = {"ahriman": package_description_ahriman}
return Package(
base="ahriman",
version="0.12.1-1",
version="1.7.0-1",
aur_url="https://aur.archlinux.org",
packages=packages)
@ -139,9 +168,16 @@ def package_description_ahriman() -> PackageDescription:
architecture="x86_64",
archive_size=4200,
build_date=42,
depends=["devtools", "git", "pyalpm", "python-aur", "python-srcinfo"],
depends=[
"devtools",
"git",
"pyalpm",
"python-aur",
"python-passlib",
"python-srcinfo",
],
description="ArcH Linux ReposItory MANager",
filename="ahriman-0.12.1-1-any.pkg.tar.zst",
filename="ahriman-1.7.0-1-any.pkg.tar.zst",
groups=[],
installed_size=4200000,
licenses=["GPL3"],

View File

@ -0,0 +1,12 @@
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,173 @@
import json
import pytest
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.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_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
"""
response = _get_response(resource_path_root)
assert AUR.parse_response(json.loads(response)) == [aur_package_ahriman]
def test_parse_response_error(resource_path_root: Path) -> None:
"""
must raise exception on invalid response
"""
response = (resource_path_root / "models" / "aur_error").read_text()
with pytest.raises(InvalidPackageInfo, match="Incorrect request type specified."):
AUR.parse_response(json.loads(response))
def test_parse_response_unknown_error(resource_path_root: Path) -> None:
"""
must raise exception on invalid response with empty error message
"""
with pytest.raises(InvalidPackageInfo, match="Unknown API error"):
AUR.parse_response({"type": "error"})
def test_make_request(aur: AUR, aur_package_ahriman: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must perform request to AUR
"""
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 aur.make_request("info", "ahriman") == [aur_package_ahriman]
request_mock.assert_called_once_with(
"https://aur.archlinux.org/rpc", params={"v": "5", "type": "info", "arg": ["ahriman"]})
def test_make_request_multi_arg(aur: AUR, aur_package_ahriman: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must perform request to AUR with multiple args
"""
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 aur.make_request("search", "ahriman", "is", "cool") == [aur_package_ahriman]
request_mock.assert_called_once_with(
"https://aur.archlinux.org/rpc", params={"v": "5", "type": "search", "arg[]": ["ahriman", "is", "cool"]})
def test_make_request_with_kwargs(aur: AUR, aur_package_ahriman: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must perform request to AUR with named parameters
"""
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 aur.make_request("search", "ahriman", by="name") == [aur_package_ahriman]
request_mock.assert_called_once_with(
"https://aur.archlinux.org/rpc", params={"v": "5", "type": "search", "arg": ["ahriman"], "by": "name"})
def test_make_request_failed(aur: AUR, mocker: MockerFixture) -> None:
"""
must reraise generic exception
"""
mocker.patch("requests.get", side_effect=Exception())
with pytest.raises(Exception):
aur.make_request("info", "ahriman")
def test_make_request_failed_http_error(aur: AUR, mocker: MockerFixture) -> None:
"""
must reraise http exception
"""
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
with pytest.raises(requests.exceptions.HTTPError):
aur.make_request("info", "ahriman")
def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for info
"""
request_mock = mocker.patch("ahriman.core.alpm.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)
def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
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")

View File

@ -86,6 +86,23 @@ def test_fetch_new(mocker: MockerFixture) -> None:
])
def test_fetch_new_without_remote(mocker: MockerFixture) -> None:
"""
must fetch nothing in case if no remote set
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
Sources.fetch(local, None)
check_output_mock.assert_has_calls([
mock.call("git", "checkout", "--force", Sources._branch,
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)),
mock.call("git", "reset", "--hard", f"origin/{Sources._branch}",
exception=None, cwd=local, logger=pytest.helpers.anyvar(int))
])
def test_has_remotes(mocker: MockerFixture) -> None:
"""
must ask for remotes

View File

@ -11,6 +11,14 @@ from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package
def test_load_archives(executor: Executor) -> None:
"""
must raise NotImplemented for missing load_archives method
"""
with pytest.raises(NotImplementedError):
executor.load_archives([])
def test_packages(executor: Executor) -> None:
"""
must raise NotImplemented for missing method
@ -182,11 +190,13 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
"""
must run update process
"""
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
move_mock = mocker.patch("shutil.move")
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
# must return complete
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])
@ -201,6 +211,8 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
# must clear directory
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_packages.assert_called_once()
# clear removed packages
remove_mock.assert_called_once_with([])
def test_process_update_group(executor: Executor, package_python_schedule: Package,
@ -209,9 +221,11 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa
must group single packages under one base
"""
mocker.patch("shutil.move")
mocker.patch("ahriman.models.package.Package.load", return_value=package_python_schedule)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_python_schedule])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
executor.process_update([package.filepath for package in package_python_schedule.packages.values()])
repo_add_mock.assert_has_calls([
@ -219,6 +233,7 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa
for package in package_python_schedule.packages.values()
], any_order=True)
status_client_mock.assert_called_once_with(package_python_schedule)
remove_mock.assert_called_once_with([])
def test_process_empty_filename(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -226,7 +241,8 @@ def test_process_empty_filename(executor: Executor, package_ahriman: Package, mo
must skip update for package which does not have path
"""
package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
@ -235,18 +251,27 @@ def test_process_update_failed(executor: Executor, package_ahriman: Package, moc
must process update for failed package
"""
mocker.patch("shutil.move", side_effect=Exception())
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed")
executor.process_update([package.filepath for package in package_ahriman.packages.values()])
status_client_mock.assert_called_once()
def test_process_update_failed_on_load(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_process_update_removed_package(executor: Executor, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must process update even with failed package load
must remove packages which have been removed from the new base
"""
mocker.patch("shutil.move")
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
without_python2 = Package.from_json(package_python_schedule.view())
del without_python2.packages["python2-schedule"]
assert executor.process_update([package.filepath for package in package_ahriman.packages.values()])
mocker.patch("shutil.move")
mocker.patch("ahriman.core.alpm.repo.Repo.add")
mocker.patch("ahriman.core.repository.executor.Executor.load_archives", return_value=[without_python2])
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
executor.process_update([package.filepath for package in without_python2.packages.values()])
remove_mock.assert_called_once_with(["python2-schedule"])

View File

@ -5,8 +5,8 @@ from ahriman.core.repository import Repository
from ahriman.models.package import Package
def test_packages(package_ahriman: Package, package_python_schedule: Package,
repository: Repository, mocker: MockerFixture) -> None:
def test_load_archives(package_ahriman: Package, package_python_schedule: Package,
repository: Repository, mocker: MockerFixture) -> None:
"""
must return all packages grouped by package base
"""
@ -17,12 +17,9 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package,
packages={package: props})
for package, props in package_python_schedule.packages.items()
] + [package_ahriman]
mocker.patch("pathlib.Path.iterdir",
return_value=[Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
mocker.patch("ahriman.models.package.Package.load", side_effect=single_packages)
packages = repository.packages()
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
assert len(packages) == 2
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
@ -33,21 +30,48 @@ def test_packages(package_ahriman: Package, package_python_schedule: Package,
assert set(archives) == expected
def test_packages_failed(repository: Repository, mocker: MockerFixture) -> None:
def test_load_archives_failed(repository: Repository, mocker: MockerFixture) -> None:
"""
must skip packages which cannot be loaded
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.pkg.tar.xz")])
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
assert not repository.packages()
assert not repository.load_archives([Path("a.pkg.tar.xz")])
def test_packages_not_package(repository: Repository, mocker: MockerFixture) -> None:
def test_load_archives_not_package(repository: Repository) -> None:
"""
must skip not packages from iteration
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz")])
assert not repository.packages()
assert not repository.load_archives([Path("a.tar.xz")])
def test_load_archives_different_version(repository: Repository, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must load packages with different versions choosing maximal
"""
single_packages = [
Package(base=package_python_schedule.base,
version=package_python_schedule.version,
aur_url=package_python_schedule.aur_url,
packages={package: props})
for package, props in package_python_schedule.packages.items()
]
single_packages[0].version = "0.0.1-1"
mocker.patch("ahriman.models.package.Package.load", side_effect=single_packages)
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")])
assert len(packages) == 1
assert packages[0].version == package_python_schedule.version
def test_packages(repository: Repository, mocker: MockerFixture) -> None:
"""
must return repository packages
"""
load_mock = mocker.patch("ahriman.core.repository.repository.Repository.load_archives")
repository.packages()
load_mock.assert_called_once() # it uses filter object so we cannot verity argument list =/
def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
@ -56,3 +80,23 @@ def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")])
assert repository.packages_built() == [Path("b.pkg.tar.xz")]
def test_packages_depends_on(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must filter packages by depends list
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
assert repository.packages_depends_on(["python-aur"]) == [package_ahriman]
def test_packages_depends_on_empty(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must return all packages in case if no filter is provided
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
assert repository.packages_depends_on(None) == [package_ahriman, package_python_schedule]

View File

@ -81,6 +81,50 @@ def test_updates_aur_ignore_vcs(update_handler: UpdateHandler, package_ahriman:
package_is_outdated_mock.assert_not_called()
def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must check for updates for locally stored packages
"""
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
package_load_mock = mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
assert update_handler.updates_local() == [package_ahriman]
fetch_mock.assert_called_once_with(package_ahriman.base, remote=None)
package_load_mock.assert_called_once()
status_client_mock.assert_called_once()
def test_updates_local_unknown(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return unknown package as out-dated
"""
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown")
assert update_handler.updates_local() == [package_ahriman]
status_client_mock.assert_called_once()
def test_updates_local_with_failures(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must process local through the packages with failure
"""
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages")
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch", side_effect=Exception())
assert not update_handler.updates_local()
def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixture) -> None:
"""
requesting manual updates must clear packages directory
@ -125,7 +169,7 @@ def test_updates_manual_status_unknown(update_handler: UpdateHandler, package_ah
def test_updates_manual_with_failures(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must process through the packages with failure
must process manual through the packages with failure
"""
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])

View File

@ -4,6 +4,7 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException
@ -54,6 +55,64 @@ def test_section_name(configuration: Configuration) -> None:
assert configuration.section_name("build", "x86_64") == "build:x86_64"
def test_getlist(configuration: Configuration) -> None:
"""
must return list of string correctly
"""
configuration.set_option("build", "test_list", "a b c")
assert configuration.getlist("build", "test_list") == ["a", "b", "c"]
def test_getlist_empty(configuration: Configuration) -> None:
"""
must return list of string correctly for non-existing option
"""
assert configuration.getlist("build", "test_list", fallback=[]) == []
configuration.set_option("build", "test_list", "")
assert configuration.getlist("build", "test_list") == []
def test_getlist_single(configuration: Configuration) -> None:
"""
must return list of strings for single string
"""
configuration.set_option("build", "test_list", "a")
assert configuration.getlist("build", "test_list") == ["a"]
assert configuration.getlist("build", "test_list") == ["a"]
def test_getlist_with_spaces(configuration: Configuration) -> None:
"""
must return list of string if there is string with spaces in quotes
"""
configuration.set_option("build", "test_list", """"ahriman is" cool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """cool"""]
configuration.set_option("build", "test_list", """'ahriman is' cool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """cool"""]
def test_getlist_with_quotes(configuration: Configuration) -> None:
"""
must return list of string if there is string with quote inside quote
"""
configuration.set_option("build", "test_list", """"ahriman is" c"'"ool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """c'ool"""]
configuration.set_option("build", "test_list", """'ahriman is' c'"'ool""")
assert configuration.getlist("build", "test_list") == ["""ahriman is""", """c"ool"""]
def test_getlist_unmatched_quote(configuration: Configuration) -> None:
"""
must raise exception on unmatched quote in string value
"""
configuration.set_option("build", "test_list", """ahri"man is cool""")
with pytest.raises(ValueError):
configuration.getlist("build", "test_list")
configuration.set_option("build", "test_list", """ahri'man is cool""")
with pytest.raises(ValueError):
configuration.getlist("build", "test_list")
def test_getpath_absolute_to_absolute(configuration: Configuration) -> None:
"""
must not change path for absolute path in settings
@ -94,32 +153,6 @@ def test_getpath_without_fallback(configuration: Configuration) -> None:
assert configuration.getpath("build", "option")
def test_getlist(configuration: Configuration) -> None:
"""
must return list of string correctly
"""
configuration.set_option("build", "test_list", "a b c")
assert configuration.getlist("build", "test_list") == ["a", "b", "c"]
def test_getlist_empty(configuration: Configuration) -> None:
"""
must return list of string correctly for non-existing option
"""
assert configuration.getlist("build", "test_list", fallback=[]) == []
configuration.set_option("build", "test_list", "")
assert configuration.getlist("build", "test_list") == []
def test_getlist_single(configuration: Configuration) -> None:
"""
must return list of strings for single string
"""
configuration.set_option("build", "test_list", "a")
assert configuration.getlist("build", "test_list") == ["a"]
assert configuration.getlist("build", "test_list") == ["a"]
def test_gettype(configuration: Configuration) -> None:
"""
must extract type from variable
@ -222,6 +255,17 @@ def test_reload(configuration: Configuration, mocker: MockerFixture) -> None:
merge_mock.assert_called_once()
def test_reload_clear(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must clear current settings before configuration reload
"""
clear_mock = mocker.patch("ahriman.core.configuration.Configuration.remove_section")
sections = configuration.sections()
configuration.reload()
clear_mock.assert_has_calls([mock.call(section) for section in sections])
def test_reload_no_architecture(configuration: Configuration) -> None:
"""
must raise exception on reload if no architecture set

View File

@ -1,4 +1,3 @@
import aur
import datetime
import logging
import pytest
@ -6,45 +5,12 @@ import subprocess
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.exceptions import InvalidOption, UnsafeRun
from ahriman.core.util import aur_search, check_output, check_user, filter_json, package_like, pretty_datetime, \
pretty_size, walk
from ahriman.core.util import check_output, check_user, filter_json, package_like, pretty_datetime, pretty_size, walk
from ahriman.models.package import Package
def test_aur_search(aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None:
"""
must search in AUR with multiple words
"""
terms = ["ahriman", "is", "cool"]
search_mock = mocker.patch("aur.search", return_value=[aur_package_ahriman])
assert aur_search(*terms) == [aur_package_ahriman]
search_mock.assert_has_calls([mock.call("ahriman"), mock.call("cool")])
def test_aur_search_empty(mocker: MockerFixture) -> None:
"""
must return empty list if no long terms supplied
"""
terms = ["it", "is"]
search_mock = mocker.patch("aur.search")
assert aur_search(*terms) == []
search_mock.assert_not_called()
def test_aur_search_single(aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None:
"""
must search in AUR with one word
"""
search_mock = mocker.patch("aur.search", return_value=[aur_package_ahriman])
assert aur_search("ahriman") == [aur_package_ahriman]
search_mock.assert_called_once_with("ahriman")
def test_check_output(mocker: MockerFixture) -> None:
"""
must run command and log result
@ -127,7 +93,7 @@ def test_filter_json(package_ahriman: Package) -> None:
def test_filter_json_empty_value(package_ahriman: Package) -> None:
"""
must return empty values from object
must filter empty values from object
"""
probe = package_ahriman.view()
probe["base"] = None
@ -238,8 +204,10 @@ def test_walk(resource_path_root: Path) -> None:
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",

View File

@ -22,7 +22,7 @@ def test_calculate_hash_small(resource_path_root: Path) -> None:
must calculate checksum for path which is single chunk
"""
path = resource_path_root / "models" / "package_ahriman_srcinfo"
assert HttpUpload.calculate_hash(path) == "a55f82198e56061295d405aeb58f4062"
assert HttpUpload.calculate_hash(path) == "c0aaf6ebf95ca9206dc8ba1d8ff10af3"
def test_get_body_get_hashes() -> None:

View File

@ -31,7 +31,7 @@ def test_calculate_etag_small(resource_path_root: Path) -> None:
must calculate checksum for path which is single chunk
"""
path = resource_path_root / "models" / "package_ahriman_srcinfo"
assert S3.calculate_etag(path, _chunk_size) == "a55f82198e56061295d405aeb58f4062"
assert S3.calculate_etag(path, _chunk_size) == "c0aaf6ebf95ca9206dc8ba1d8ff10af3"
def test_files_remove(s3_remote_objects: List[Any]) -> None:

View File

@ -82,7 +82,9 @@ def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock:
"""
mock = MagicMock()
type(mock).base = PropertyMock(return_value=package_ahriman.base)
type(mock).depends = PropertyMock(return_value=["python-aur"])
type(mock).name = PropertyMock(return_value=package_ahriman.base)
type(mock).provides = PropertyMock(return_value=["python-ahriman"])
type(mock).version = PropertyMock(return_value=package_ahriman.version)
return mock

View File

@ -0,0 +1,47 @@
import datetime
import json
from dataclasses import asdict, fields
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any, Dict
from ahriman.models.aur_package import AURPackage
def _get_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_ahriman_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)
assert AURPackage.from_json(model) == aur_package_ahriman
def test_from_json_2(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must load the same package from json
"""
mocker.patch("ahriman.models.aur_package.AURPackage.convert", side_effect=lambda v: v)
assert AURPackage.from_json(asdict(aur_package_ahriman)) == aur_package_ahriman
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)
converted = AURPackage.convert(model)
known_fields = [pair.name for pair in fields(AURPackage)]
assert all(field in known_fields for field in converted)
assert isinstance(converted.get("first_submitted"), datetime.datetime)
assert isinstance(converted.get("last_modified"), datetime.datetime)

View File

@ -1,4 +1,5 @@
import datetime
import time
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -45,6 +46,17 @@ def test_build_status_init_2(build_status_failed: BuildStatus) -> None:
assert status == build_status_failed
def test_build_status_init_empty_timestamp() -> None:
"""
must st current timestamp when not set
"""
first = BuildStatus()
time.sleep(1)
second = BuildStatus()
# well technically it just should increase
assert first.timestamp < second.timestamp
def test_build_status_from_json_view(build_status_failed: BuildStatus) -> None:
"""
must construct same object from json

View File

@ -2,10 +2,12 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock, PropertyMock
from unittest.mock import MagicMock
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.models.aur_package import AURPackage
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@ -95,15 +97,11 @@ def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker
assert Package.from_archive(Path("path"), pyalpm_handle, package_ahriman.aur_url) == package_ahriman
def test_from_aur(package_ahriman: Package, mocker: MockerFixture) -> None:
def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must construct package from aur
"""
mock = MagicMock()
type(mock).name = PropertyMock(return_value=package_ahriman.base)
type(mock).package_base = PropertyMock(return_value=package_ahriman.base)
type(mock).version = PropertyMock(return_value=package_ahriman.version)
mocker.patch("aur.info", return_value=mock)
mocker.patch("ahriman.core.alpm.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
@ -156,14 +154,24 @@ 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_load_resolve(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must resolve source before package loading
"""
resolve_mock = mocker.patch("ahriman.models.package_source.PackageSource.resolve",
return_value=PackageSource.Archive)
mocker.patch("ahriman.models.package.Package.from_archive")
Package.load("path", PackageSource.Archive, pyalpm_handle, package_ahriman.aur_url)
resolve_mock.assert_called_once_with("path")
def test_load_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from package archive
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_archive")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.Archive, pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
@ -172,8 +180,7 @@ def test_load_from_aur(package_ahriman: Package, pyalpm_handle: MagicMock, mocke
must load package from AUR
"""
load_mock = mocker.patch("ahriman.models.package.Package.from_aur")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
@ -181,10 +188,8 @@ def test_load_from_build(package_ahriman: Package, pyalpm_handle: MagicMock, moc
"""
must load package from build directory
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_build")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.Local, pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
@ -192,13 +197,26 @@ def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker
"""
must raise InvalidPackageInfo on exception
"""
mocker.patch("pathlib.Path.is_dir", side_effect=InvalidPackageInfo("exception!"))
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=InvalidPackageInfo("exception!"))
with pytest.raises(InvalidPackageInfo):
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url)
mocker.patch("pathlib.Path.is_dir", side_effect=Exception())
def test_load_failure_exception(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must raise InvalidPackageInfo on random eexception
"""
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception())
with pytest.raises(InvalidPackageInfo):
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
Package.load("path", PackageSource.AUR, pyalpm_handle, package_ahriman.aur_url)
def test_load_invalid_source(package_ahriman: Package, pyalpm_handle: MagicMock) -> None:
"""
must raise InvalidPackageInfo on unsupported source
"""
with pytest.raises(InvalidPackageInfo):
Package.load("path", PackageSource.Remote, pyalpm_handle, package_ahriman.aur_url)
def test_dependencies_failed(mocker: MockerFixture) -> None:
@ -273,6 +291,24 @@ def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_p
assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version
def test_full_depends(package_ahriman: Package, package_python_schedule: Package, pyalpm_package_ahriman: MagicMock,
pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must extract all dependencies from the package
"""
package_python_schedule.packages[package_python_schedule.base].provides = ["python3-schedule"]
database_mock = MagicMock()
database_mock.pkgcache = [pyalpm_package_ahriman]
pyalpm_handle.handle.get_syncdbs.return_value = [database_mock]
assert package_ahriman.full_depends(pyalpm_handle, [package_python_schedule]) == package_ahriman.depends
package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base]
expected = sorted(set(package_python_schedule.depends + ["python-aur"]))
assert package_python_schedule.full_depends(pyalpm_handle, [package_python_schedule]) == expected
def test_is_outdated_false(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
"""
must be not outdated for the same package

View File

@ -2,7 +2,8 @@ import pytest
from aiohttp import web
from asyncio import BaseEventLoop
from pytest_aiohttp import TestClient
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from typing import Any

View File

@ -1,9 +1,9 @@
import aur
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.aur_package import AURPackage
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.search import SearchView
@ -17,11 +17,11 @@ async def test_get_permission() -> None:
assert await SearchView.get_permission(request) == UserAccess.Read
async def test_get(client: TestClient, aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None:
async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must call get request correctly
"""
mocker.patch("ahriman.web.views.service.search.aur_search", return_value=[aur_package_ahriman])
mocker.patch("ahriman.core.alpm.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.web.views.service.search.aur_search", return_value=[])
search_mock = mocker.patch("ahriman.core.alpm.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.web.views.service.search.aur_search")
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch")
response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")])
assert response.ok

View File

@ -1,6 +1,6 @@
import pytest
from pytest_aiohttp import TestClient
from aiohttp.test_utils import TestClient
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package

View File

@ -1,6 +1,6 @@
import pytest
from pytest_aiohttp import TestClient
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatusEnum

View File

@ -1,6 +1,6 @@
import pytest
from pytest_aiohttp import TestClient
from aiohttp.test_utils import TestClient
import ahriman.version as version

View File

@ -1,6 +1,6 @@
import pytest
from pytest_aiohttp import TestClient
from aiohttp.test_utils import TestClient
from ahriman.models.user_access import UserAccess
from ahriman.web.views.index import IndexView

View File

@ -1,4 +1,5 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
@ -31,7 +32,7 @@ async def test_get_redirect_to_oauth(client_with_auth: TestClient) -> None:
must redirect to OAuth service provider in case if no code is supplied
"""
oauth = client_with_auth.app["validator"] = MagicMock(spec=OAuth)
oauth.get_oauth_url.return_value = "https://example.com"
oauth.get_oauth_url.return_value = "https://httpbin.org"
get_response = await client_with_auth.get("/user-api/v1/login")
assert get_response.ok
@ -43,7 +44,7 @@ async def test_get_redirect_to_oauth_empty_code(client_with_auth: TestClient) ->
must redirect to OAuth service provider in case if empty code is supplied
"""
oauth = client_with_auth.app["validator"] = MagicMock(spec=OAuth)
oauth.get_oauth_url.return_value = "https://example.com"
oauth.get_oauth_url.return_value = "https://httpbin.org"
get_response = await client_with_auth.get("/user-api/v1/login", params={"code": ""})
assert get_response.ok

View File

@ -0,0 +1,7 @@
{
"error": "Incorrect request type specified.",
"resultcount": 0,
"results": [],
"type": "error",
"version": 5
}

View File

@ -0,0 +1,54 @@
{
"resultcount": 1,
"results": [
{
"Depends": [
"devtools",
"git",
"pyalpm",
"python-aur",
"python-passlib",
"python-srcinfo"
],
"Description": "ArcH Linux ReposItory MANager",
"FirstSubmitted": 1618008285,
"ID": 1009791,
"Keywords": [],
"LastModified": 1640473871,
"License": [
"GPL3"
],
"Maintainer": "arcanis",
"MakeDepends": [
"python-pip"
],
"Name": "ahriman",
"NumVotes": 0,
"OptDepends": [
"breezy",
"darcs",
"mercurial",
"python-aioauth-client",
"python-aiohttp",
"python-aiohttp-debugtoolbar",
"python-aiohttp-jinja2",
"python-aiohttp-security",
"python-aiohttp-session",
"python-boto3",
"python-cryptography",
"python-jinja",
"rsync",
"subversion"
],
"OutOfDate": null,
"PackageBase": "ahriman",
"PackageBaseID": 165427,
"Popularity": 0,
"URL": "https://github.com/arcan1s/ahriman",
"URLPath": "/cgit/aur.git/snapshot/ahriman.tar.gz",
"Version": "1.7.0-1"
}
],
"type": "multiinfo",
"version": 5
}

View File

@ -1,6 +1,6 @@
pkgbase = ahriman
pkgdesc = ArcH Linux ReposItory MANager
pkgver = 0.12.1
pkgver = 1.7.0
pkgrel = 1
url = https://github.com/arcan1s/ahriman
arch = any
@ -10,6 +10,7 @@ pkgbase = ahriman
depends = git
depends = pyalpm
depends = python-aur
depends = python-passlib
depends = python-srcinfo
optdepends = aws-cli: sync to s3
optdepends = breezy: -bzr packages support
@ -24,7 +25,7 @@ pkgbase = ahriman
optdepends = subversion: -svn packages support
backup = etc/ahriman.ini
backup = etc/ahriman.ini.d/logging.ini
source = https://github.com/arcan1s/ahriman/releases/download/0.12.1/ahriman-0.12.1-src.tar.xz
source = https://github.com/arcan1s/ahriman/releases/download/1.7.0/ahriman-1.7.0-src.tar.xz
source = ahriman.sysusers
source = ahriman.tmpfiles
sha512sums = 8acc57f937d587ca665c29092cadddbaf3ba0b80e870b80d1551e283aba8f21306f9030a26fec8c71ab5863316f5f5f061b7ddc63cdff9e6d5a885f28ef1893d