rewrite repository definition logic

This commit is contained in:
2023-08-31 03:49:40 +03:00
committed by Evgenii Alekseev
parent b3730890a2
commit 3007e129c5
45 changed files with 668 additions and 241 deletions

View File

@ -157,7 +157,8 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--sort-by", help="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",
default="name", choices=sorted(handlers.Search.SORT_FIELDS))
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, report=False, quiet=True, unsafe=True)
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, report=False, repository=[""],
quiet=True, unsafe=True)
return parser
@ -175,8 +176,8 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="show help message for application or command and exit",
formatter_class=_formatter)
parser.add_argument("command", help="show help message for specific command", nargs="?")
parser.set_defaults(handler=handlers.Help, architecture=[""], lock=None, report=False, quiet=True, unsafe=True,
parser=_parser)
parser.set_defaults(handler=handlers.Help, architecture=[""], lock=None, report=False, repository=[""], quiet=True,
unsafe=True, parser=_parser)
return parser
@ -194,8 +195,8 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument
description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, quiet=True,
unsafe=True, parser=_parser)
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, repository=[""],
quiet=True, unsafe=True, parser=_parser)
return parser
@ -213,8 +214,8 @@ def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="request AUR for current version and compare with current service version",
formatter_class=_formatter)
parser.add_argument("-e", "--exit-code", help="return non-zero exit code if updates available", action="store_true")
parser.set_defaults(handler=handlers.ServiceUpdates, architecture=[""], lock=None, report=False, quiet=True,
unsafe=True)
parser.set_defaults(handler=handlers.ServiceUpdates, architecture=[""], lock=None, report=False, repository=[""],
quiet=True, unsafe=True)
return parser
@ -230,7 +231,8 @@ def _set_help_version_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("help-version", aliases=["version"], help="application version",
description="print application and its dependencies versions", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, report=False, quiet=True, unsafe=True)
parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, report=False, repository=[""],
quiet=True, unsafe=True)
return parser
@ -381,7 +383,8 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"it must end with ()")
parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
"the value will be read from stdin", type=Path, nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False)
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False,
repository=[""])
return parser
@ -402,7 +405,7 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, report=False,
unsafe=True)
repository=[""], unsafe=True)
return parser
@ -423,7 +426,8 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"to remove only specified PKGBUILD variables. In case if not set, "
"it will remove all patches related to the package",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, report=False)
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, report=False,
repository=[""])
return parser
@ -448,7 +452,7 @@ def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False,
variable=None)
repository=[""], variable=None)
return parser
@ -465,7 +469,8 @@ def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("repo-backup", help="backup repository data",
description="backup repository settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the output archive", type=Path)
parser.set_defaults(handler=handlers.Backup, architecture=[""], lock=None, report=False, unsafe=True)
parser.set_defaults(handler=handlers.Backup, architecture=[""], lock=None, report=False, repository=[""],
unsafe=True)
return parser
@ -644,7 +649,8 @@ def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="restore settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the input archive", type=Path)
parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/"))
parser.set_defaults(handler=handlers.Restore, architecture=[""], lock=None, report=False, unsafe=True)
parser.set_defaults(handler=handlers.Restore, architecture=[""], lock=None, report=False, repository=[""],
unsafe=True)
return parser
@ -681,7 +687,7 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa
description="update repository status on the status page", formatter_class=_formatter)
parser.add_argument("-s", "--status", help="new status",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, report=False, package=[],
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, package=[], report=False,
quiet=True, unsafe=True)
return parser
@ -865,7 +871,7 @@ def _set_service_key_import_parser(root: SubParserAction) -> argparse.ArgumentPa
formatter_class=_formatter)
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False)
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False, repository=[""])
return parser
@ -946,7 +952,7 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-R", "--role", help="user access level",
type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read)
parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, report=False,
quiet=True)
repository=[""], quiet=True)
return parser
@ -967,7 +973,7 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-R", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess))
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False,
quiet=True, unsafe=True)
repository=[""], quiet=True, unsafe=True)
return parser
@ -986,7 +992,7 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False,
quiet=True)
repository=[""], quiet=True)
return parser

View File

@ -34,7 +34,7 @@ class Backup(Handler):
backup packages handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -30,7 +30,7 @@ class Dump(Handler):
dump configuration handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -35,7 +35,6 @@ class Handler:
base handler class for command callbacks
Attributes:
ALLOW_AUTO_ARCHITECTURE_RUN(bool): (class attribute) allow defining architecture from existing repositories
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures
Examples:
@ -47,7 +46,6 @@ class Handler:
>>> Add.execute(args)
"""
ALLOW_AUTO_ARCHITECTURE_RUN = True
ALLOW_MULTI_ARCHITECTURE_RUN = True
@classmethod
@ -121,32 +119,35 @@ class Handler:
Raises:
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
"""
if not cls.ALLOW_AUTO_ARCHITECTURE_RUN and args.architecture is None:
# for some parsers (e.g. config) we need to run with specific architecture
# for those cases architecture must be set explicitly
raise MissingArchitectureError(args.command)
configuration = Configuration()
configuration.load(args.configuration)
name = configuration.get("repository", "name", fallback="") # will only be used for legacy mode
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
if args.architecture: # architecture is specified explicitly
repositories = args.repository or [name] # fallback for legacy mode
return sorted(
set(
RepositoryId(architecture, repository)
for architecture in args.architecture
for repository in repositories
)
# extract repository names first
names = args.repository
if names is None: # try to read file system first
names = RepositoryPaths.known_repositories(root)
if not names: # try to read configuration now
names = [configuration.get("repository", "name")]
# extract architecture names
if (architectures := args.architecture) is not None:
repositories = set(
RepositoryId(architecture, name)
for name in names
for architecture in architectures
)
else: # try to read from file system
repositories = set(
RepositoryId(architecture, name)
for name in names
for architecture in RepositoryPaths.known_architectures(root, name)
)
# wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
architectures = RepositoryPaths.known_architectures(root, name)
if not architectures: # well we did not find anything
if not repositories:
raise MissingArchitectureError(args.command)
return sorted(architectures)
return sorted(repositories)
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -29,7 +29,7 @@ class Help(Handler):
help handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -30,7 +30,7 @@ class KeyImport(Handler):
key import packages handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -38,6 +38,8 @@ class Patch(Handler):
patch control handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:

View File

@ -31,7 +31,7 @@ class Restore(Handler):
restore packages handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -40,7 +40,7 @@ class Search(Handler):
SORT_FIELDS(set[str]): (class attribute) allowed fields to sort the package list
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
SORT_FIELDS = {
field.name
for field in fields(AURPackage)

View File

@ -33,7 +33,7 @@ class ServiceUpdates(Handler):
service updates handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -25,6 +25,7 @@ from pwd import getpwuid
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitectureError
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user import User
@ -40,7 +41,7 @@ class Setup(Handler):
SUDOERS_DIR_PATH(Path): (class attribute) path to sudoers.d includes directory
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
ARCHBUILD_COMMAND_PATH = Path("/usr") / "bin" / "archbuild"
MIRRORLIST_PATH = Path("/etc") / "pacman.d" / "mirrorlist"
@ -58,6 +59,10 @@ class Setup(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
# special check for args to avoid auto definition for setup command
if args.architecture is None or args.repository is None:
raise MissingArchitectureError(args.command)
Setup.configuration_create_ahriman(args, repository_id, configuration)
configuration.reload()

View File

@ -34,7 +34,7 @@ class Shell(Handler):
python shell handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -35,7 +35,7 @@ class Status(Handler):
package status handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -31,7 +31,7 @@ class StatusUpdate(Handler):
status update handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -32,7 +32,7 @@ class Structure(Handler):
dump repository structure handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -30,7 +30,7 @@ class UnsafeCommands(Handler):
unsafe command help parser
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -35,7 +35,7 @@ class Users(Handler):
user management handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -37,7 +37,7 @@ class Validate(Handler):
configuration validator handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -39,7 +39,7 @@ class Versions(Handler):
PEP423_PACKAGE_NAME(str): (class attribute) special regex for valid PEP423 package name
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+")
@classmethod

View File

@ -32,7 +32,6 @@ class Web(Handler):
web server handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
@classmethod

View File

@ -165,7 +165,7 @@ class MissingArchitectureError(ValueError):
Args:
command(str): command name which throws exception
"""
ValueError.__init__(self, f"Architecture required for subcommand {command}, but missing")
ValueError.__init__(self, f"Architecture/repository required for subcommand {command}, but missing")
class MultipleArchitecturesError(ValueError):
@ -180,7 +180,7 @@ class MultipleArchitecturesError(ValueError):
Args:
command(str): command name which throws exception
"""
ValueError.__init__(self, f"Multiple architectures are not supported by subcommand {command}")
ValueError.__init__(self, f"Multiple architectures/repositories are not supported by subcommand {command}")
class OptionError(ValueError):

View File

@ -20,8 +20,6 @@
from dataclasses import dataclass
from typing import Any
from ahriman.core.exceptions import InitializeError
@dataclass(frozen=True)
class RepositoryId:
@ -36,16 +34,6 @@ class RepositoryId:
architecture: str
name: str
def __post_init__(self) -> None:
"""
check that name is set
Raises:
InitializeError: in case if name is not set
"""
if not self.name:
raise InitializeError("Repository name is not set")
def __lt__(self, other: Any) -> bool:
"""
comparison operator for sorting

View File

@ -140,31 +140,57 @@ class RepositoryPaths:
"""
return self.owner(self.root)
# TODO see https://github.com/python/mypy/issues/12534, remove type: ignore after release
# pylint: disable=protected-access
@classmethod
def known_architectures(cls, root: Path, name: str) -> set[RepositoryId]:
def known_architectures(cls, root: Path, name: str = "") -> set[str]: # type: ignore[return]
"""
get known architectures
get known architecture names
Args:
root(Path): repository root
name(str): repository name from configuration
name(str, optional): repository name (Default value = "")
Returns:
set[RepositoryId]: list of tuple of repository name and architectures for which tree is created
set[str]: list of repository architectures for which there is created tree
"""
def walk(repository_path: Path, repository_name: str) -> Generator[RepositoryId, None, None]:
for architecture in filter(lambda path: path.is_dir(), repository_path.iterdir()):
yield RepositoryId(architecture.name, repository_name)
def walk(repository_dir: Path) -> Generator[str, None, None]:
for architecture in filter(lambda path: path.is_dir(), repository_dir.iterdir()):
yield architecture.name
def walk_root(paths: RepositoryPaths) -> Generator[RepositoryId, None, None]:
# pylint: disable=protected-access
instance = cls(root, RepositoryId("", ""))
match (instance._repository_root / name):
case full_tree if full_tree.is_dir():
return set(walk(full_tree)) # actually works for legacy too in case if name is set to empty string
case _ if instance._repository_root.is_dir():
return set(walk(instance._repository_root)) # legacy only tree
case _:
return set() # no tree detected at all
# pylint: disable=protected-access
@classmethod
def known_repositories(cls, root: Path) -> set[str]:
"""
get known repository names
Args:
root(Path): repository root
Returns:
set[str]: list of repository names for which there is created tree. Returns empty set in case if repository
is loaded in legacy mode
"""
# simply walk through the root. In case if there are subdirectories, emit the name
def walk(paths: RepositoryPaths) -> Generator[str, None, None]:
for repository in filter(lambda path: path.is_dir(), paths._repository_root.iterdir()):
print(repository)
yield from walk(repository, repository.name)
if any(path.is_dir() for path in repository.iterdir()):
yield repository.name
instance = cls(root, RepositoryId("", root.name)) # suppress initialization error
# try to get list per repository first and then fallback to old schema if nothing found
return set(walk_root(instance)) or set(walk(instance._repository_root, name))
instance = cls(root, RepositoryId("", ""))
if not instance._repository_root.is_dir():
return set() # no tree created
return set(walk(instance))
@staticmethod
def owner(path: Path) -> tuple[int, int]: