initial import

This commit is contained in:
Evgenii Alekseev 2021-03-02 12:17:01 +03:00
commit 53d21d6496
29 changed files with 888 additions and 0 deletions

96
.gitignore vendored Normal file
View File

@ -0,0 +1,96 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
parts/
sdist/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
*.deb
.idea/
.mypy_cache/
.venv/
*.tar.xz

35
make_release.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
set -e
VERSION="$1"
ARCHIVE="ahriman"
FILES="package src setup.py"
IGNORELIST="build .idea package/archlinux package/*src.tar.xz"
# set version
sed -i "/__version__ = '[0-9.]*/s/[^'][^)]*/__version__ = '$VERSION'/" src/ahriman/version.py
# create archive
[[ -e ${ARCHIVE}-${VERSION}-src.tar.xz ]] && rm -f "${ARCHIVE}-${VERSION}-src.tar.xz"
[[ -d $ARCHIVE ]] && rm -rf "$ARCHIVE"
mkdir "$ARCHIVE"
for FILE in ${FILES[*]}; do cp -r "$FILE" "$ARCHIVE"; done
for FILE in ${IGNORELIST[*]}; do rm -rf "${ARCHIVE}/${FILE}"; done
tar cJf "${ARCHIVE}-${VERSION}-src.tar.xz" "$ARCHIVE"
rm -rf "$ARCHIVE"
# update checksums
SHA512SUMS=$(sha512sum ${ARCHIVE}-${VERSION}-src.tar.xz | awk '{print $1}')
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$SHA512SUMS'/" package/archlinux/PKGBUILD
sed -i "s/pkgver=[0-9.]*/pkgver=$VERSION/" package/archlinux/PKGBUILD
# clear
find . -type f -name '*src.tar.xz' -not -name "*${VERSION}-src.tar.xz" -exec rm -rf {} \;
exit 0
# tag
git add package/archlinux/PKGBUILD
git commit -m "Release $VERSION" && git push
git tag $VERSION && git push --tags

View File

@ -0,0 +1,38 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=0.1.0
pkgrel=1
pkgdesc="ArcHlinux ReposItory MANager"
arch=('any')
url="https://github.com/arcan1s/ahriman"
license=('GPL3')
depends=('devtools' 'python-aur' 'python-srcinfo')
makedepends=('python-pip')
optdepends=('gnupg: package and repository sign support')
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sudoers'
'ahriman.sysusers'
'ahriman.tmpfiles')
sha512sums=('d42d779279493c0de86f8e6880cd644a2d549d61cf6c03c27706a155ca4350158d9a309ac77377de13002071727f2e8532144fb3aa1f2ff95811bd9f3cffd9f3'
'8c9b5b63ac3f7b4d9debaf801a1e9c060877c33d3ecafe18010fcca778e5fa2f2e46909d3d0ff1b229ff8aa978445d8243fd36e1fc104117ed678d5e21901167'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini'
'etc/ahriman.ini.d/logging.ini')
build() {
cd "$pkgname"
python setup.py build
}
package() {
cd "$pkgname"
python setup.py install --root="$pkgdir"
install -Dm400 "$srcdir/$pkgname.sudoers" "$pkgdir/etc/sudoers.d/$pkgname"
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
}

View File

@ -0,0 +1,4 @@
# Used by ArcHlinux ReposItory MANager with default settings
Cmnd_Alias ARCHBUILD_CMD = /usr/bin/extra-x86_64-build *, /usr/bin/multilib-build *
ahriman ALL=(ALL) NOPASSWD: ARCHBUILD_CMD

View File

@ -0,0 +1 @@
u ahriman 643 "ArcHlinux ReposItory MANager" /var/lib/ahriman

View File

@ -0,0 +1,2 @@
d /var/lib/ahriman 0775 ahriman log
d /var/log/ahriman 0755 ahriman ahriman

3
package/bin/ahriman Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
exec python -B -m ahriman.application.ahriman "$@"

20
package/etc/ahriman.ini Normal file
View File

@ -0,0 +1,20 @@
[settings]
include = /etc/ahriman.ini.d
logging = /etc/ahriman.ini.d/logging.ini
[aur]
url = https://aur.archlinux.org
[build]
archbuild_flags = -c
extra_build = extra-x86_64-build
makepkg_flags = --skippgpcheck
multilib_build = multilib-build
[repository]
name = aur-clone
root = /var/lib/ahriman
[sign]
enabled = disabled
key =

View File

@ -0,0 +1,47 @@
[loggers]
keys = root,builder,build_details
[handlers]
keys = console_handler,build_file_handler,file_handler
[formatters]
keys = generic_format
[handler_console_handler]
class = StreamHandler
level = DEBUG
formatter = generic_format
args = (sys.stdout,)
[handler_file_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
args = ('/var/log/ahriman/ahriman.log', 'a', 20971520, 20)
[handler_build_file_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
args = ('/var/log/ahriman/build.log', 'a', 20971520, 20)
[formatter_generic_format]
format = %(asctime)s : %(levelname)s : %(funcName)s : %(message)s
datefmt =
[logger_root]
level = DEBUG
handlers = file_handler
qualname = root
[logger_builder]
level = DEBUG
handlers = file_handler
qualname = builder
propagate = 0
[logger_build_details]
level = DEBUG
handlers = build_file_handler
qualname = build_details
propagate = 0

View File

@ -0,0 +1,7 @@
[Unit]
Description=ArcHlinux ReposItory MANager
[Service]
ExecStart=/usr/bin/ahriman update
User=ahriman
Group=ahriman

View File

@ -0,0 +1,9 @@
[Unit]
Description=ArcHlinux ReposItory MANager timer
[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
[Install]
WantedBy=timers.target

56
setup.py Normal file
View File

@ -0,0 +1,56 @@
from distutils.util import convert_path
from setuptools import setup, find_packages
from os import path
here = path.abspath(path.dirname(__file__))
metadata = dict()
with open(convert_path('src/ahriman/version.py')) as metadata_file:
exec(metadata_file.read(), metadata)
setup(
name='ahriman',
version=metadata['__version__'],
zip_safe=False,
description='ArcHlinux ReposItory MANager',
author='arcanis',
author_email='',
url='',
license='GPL3',
packages=find_packages('src'),
package_dir={'': 'src'},
dependency_links=[
],
install_requires=[
'aur',
'srcinfo',
],
setup_requires=[
'pytest-runner',
],
tests_require=[
'pytest',
],
include_package_data=True,
scripts=[
'package/bin/ahriman'
],
data_files=[
('/etc', ['package/etc/ahriman.ini']),
('/etc/ahriman.ini.d', ['package/etc/ahriman.ini.d/logging.ini']),
('lib/systemd/system', [
'package/lib/systemd/system/ahriman.service',
'package/lib/systemd/system/ahriman.timer'
])
],
extras_require={
'test': ['coverage', 'pytest'],
},
)

0
src/ahriman/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,76 @@
import argparse
import os
import ahriman.version as version
from ahriman.application.application import Application
from ahriman.core.configuration import Configuration
def _get_app(args: argparse.Namespace) -> Application:
config = _get_config(args.config)
return Application(config)
def _get_config(config_path: str) -> Configuration:
config = Configuration()
config.load(config_path)
config.load_logging()
return config
def _remove_lock(path: str) -> None:
try:
os.remove(path)
except FileNotFoundError:
pass
def add(args: argparse.Namespace) -> None:
_get_app(args).add(args.package)
def remove(args: argparse.Namespace) -> None:
_get_app(args).remove(args.package)
def update(args: argparse.Namespace) -> None:
_get_app(args).update()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='ArcHlinux ReposItory MANager')
parser.add_argument('-c', '--config', help='configuration path', default='/etc/ahriman.ini')
parser.add_argument('--force', help='force run, remove file lock', action='store_true')
parser.add_argument('--lock', help='lock file', default='/tmp/ahriman.lock')
parser.add_argument('-v', '--version', action='version', version=version.__version__)
subparsers = parser.add_subparsers(title='commands')
add_parser = subparsers.add_parser('add', description='add package')
add_parser.add_argument('package', help='package name', nargs='+')
add_parser.set_defaults(fn=add)
remove_parser = subparsers.add_parser('remove', description='remove package')
remove_parser.add_argument('package', help='package name', nargs='+')
remove_parser.set_defaults(fn=remove)
update_parser = subparsers.add_parser('update', description='run updates')
update_parser.set_defaults(fn=update)
args = parser.parse_args()
if args.force:
_remove_lock(args.lock)
if os.path.exists(args.lock):
raise RuntimeError('Another application instance is run')
if 'fn' not in args:
parser.print_help()
exit(1)
try:
open(args.lock, 'w').close()
args.fn(args)
finally:
_remove_lock(args.lock)

View File

@ -0,0 +1,29 @@
import os
from typing import List
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.core.task import Task
from ahriman.models.package import Package
class Application:
def __init__(self, config: Configuration) -> None:
self.config = config
self.repository = Repository(config)
def add(self, names: List[str]) -> None:
for name in names:
package = Package.load(name, self.config.get('aur', 'url'))
task = Task(package, self.config, self.repository.paths)
task.fetch(os.path.join(self.repository.paths.manual, package.name))
def remove(self, names: List[str]) -> None:
self.repository.process_remove(names)
def update(self) -> None:
updates = self.repository.updates()
packages = self.repository.process_build(updates)
self.repository.process_update(packages)

View File

View File

@ -0,0 +1,40 @@
import configparser
import os
from logging.config import fileConfig
from typing import Any, Dict, Optional
from ahriman.core.exceptions import MissingConfiguration
# built-in configparser extension
class Configuration(configparser.RawConfigParser):
def __init__(self) -> None:
configparser.RawConfigParser.__init__(self, allow_no_value=True)
self.path = None # type: Optional[str]
@property
def include(self) -> str:
return self.get('settings', 'include')
def get_section(self, section: str) -> Dict[str, str]:
if not self.has_section(section):
raise MissingConfiguration(section)
return dict(self[section])
def load(self, path: str) -> None:
self.path = path
self.read(self.path)
self.load_includes()
def load_includes(self) -> None:
try:
include_dir = self.include
for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(include_dir))):
self.read(os.path.join(self.include, conf))
except (FileNotFoundError, configparser.NoOptionError):
pass
def load_logging(self) -> None:
fileConfig(self.get('settings', 'logging'))

View File

@ -0,0 +1,21 @@
from typing import Any
class BuildFailed(Exception):
def __init__(self, package: str) -> None:
Exception.__init__(self, f'Package {package} build failed, check logs for details')
class InvalidOptionException(Exception):
def __init__(self, value: Any) -> None:
Exception.__init__(self, f'Invalid or unknown option value `{value}`')
class InvalidPackageInfo(Exception):
def __init__(self, details: Any) -> None:
Exception.__init__(self, f'There are errors during reading package information: `{details}`')
class MissingConfiguration(Exception):
def __init__(self, name: str) -> None:
Exception.__init__(self, f'No section `{name}` found')

View File

@ -0,0 +1,40 @@
import logging
import os
from ahriman.core.exceptions import BuildFailed
from ahriman.core.util import check_output
from ahriman.models.repository_paths import RepositoryPaths
class RepoWrapper:
def __init__(self, name: str, paths: RepositoryPaths) -> None:
self.logger = logging.getLogger('build_details')
self.name = name
self.paths = paths
@property
def repo_path(self) -> str:
return os.path.join(self.paths.repository, f'{self.name}.db.tar.gz')
def add(self, path: str) -> None:
check_output(
'repo-add', '-R', self.repo_path, path,
exception=BuildFailed(path),
cwd=self.paths.repository,
logger=self.logger)
def remove(self, path: str, package: str) -> None:
try:
os.remove(path)
except FileNotFoundError:
pass
try:
os.remove(f'{path}.sig') # sign if any
except FileNotFoundError:
pass
check_output(
'repo-remove', self.repo_path, package,
exception=BuildFailed(path),
cwd=self.paths.repository,
logger=self.logger)

View File

@ -0,0 +1,123 @@
import logging
import os
import shutil
from typing import List
from ahriman.core.configuration import Configuration
from ahriman.core.repo_wrapper import RepoWrapper
from ahriman.core.sign import Sign
from ahriman.core.task import Task
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class Repository:
def __init__(self, config: Configuration) -> None:
self.logger = logging.getLogger('builder')
self.config = config
self.aur_url = config.get('aur', 'url')
self.name = config.get('repository', 'name')
self.paths = RepositoryPaths(config.get('repository', 'root'))
self.paths.create_tree()
self.sign = Sign(config)
self.wrapper = RepoWrapper(self.name, self.paths)
def _clear_build(self) -> None:
for package in os.listdir(self.paths.sources):
shutil.rmtree(os.path.join(self.paths.sources, package), ignore_errors=True)
def _clear_manual(self) -> None:
for package in os.listdir(self.paths.manual):
shutil.rmtree(os.path.join(self.paths.manual, package), ignore_errors=True)
def _clear_packages(self) -> None:
for package in os.listdir(self.paths.packages):
shutil.rmtree(os.path.join(self.paths.packages, package), ignore_errors=True)
def process_build(self, updates: List[Package]) -> List[str]:
for package in updates:
try:
task = Task(package, self.config, self.paths)
task.fetch()
built = task.build()
for src in built:
dst = os.path.join(self.paths.packages, os.path.basename(src))
shutil.move(src, dst)
except Exception:
self.logger.exception(f'{package.name} build exception', exc_info=True)
continue
self._clear_build()
return [
os.path.join(self.paths.packages, fn)
for fn in os.listdir(self.paths.packages)
]
def process_remove(self, packages: List[str]) -> str:
for fn in os.listdir(self.paths.repository):
if '.pkg.' not in fn:
continue
full_path = os.path.join(self.paths.repository, fn)
try:
local = Package.load(full_path, self.aur_url)
if local.name not in packages:
continue
self.wrapper.remove(full_path, local.name)
except Exception:
self.logger.exception(f'could not load package from {fn}', exc_info=True)
continue
self.sign.sign_repository(self.wrapper.repo_path)
return self.wrapper.repo_path
def process_update(self, packages: List[str]) -> str:
for package in packages:
files = self.sign.sign_package(package)
for src in files:
dst = os.path.join(self.paths.repository, os.path.basename(src))
shutil.move(src, dst)
package_fn = os.path.join(self.paths.repository, os.path.basename(package))
self.wrapper.add(package_fn)
self._clear_packages()
self.sign.sign_repository(self.wrapper.repo_path)
return self.wrapper.repo_path
def updates(self) -> List[Package]:
result: List[Package] = []
checked_base: List[str] = []
# repository updates
for fn in os.listdir(self.paths.repository):
if '.pkg.' not in fn:
continue
try:
local = Package.load(os.path.join(self.paths.repository, fn), self.aur_url)
remote = Package.load(local.name, self.aur_url)
except Exception:
self.logger.exception(f'could not load package from {fn}', exc_info=True)
continue
if local.name in checked_base:
continue
if local.is_outdated(remote):
result.append(remote)
checked_base.append(local.name)
# manual updates
for fn in os.listdir(self.paths.manual):
local = Package.load(os.path.join(self.paths.manual, fn), self.aur_url)
if local.name in checked_base:
continue
result.append(local)
checked_base.append(local.name)
self._clear_manual()
return result

44
src/ahriman/core/sign.py Normal file
View File

@ -0,0 +1,44 @@
import logging
import os
from typing import List
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildFailed
from ahriman.core.util import check_output
from ahriman.models.sign_settings import SignSettings
class Sign:
def __init__(self, config: Configuration) -> None:
self.logger = logging.getLogger('build_details')
self.key = config.get('sign', 'key', fallback=None)
self.sign = SignSettings.from_option(config.get('sign', 'enabled'))
def process(self, path: str) -> List[str]:
cwd = os.path.dirname(path)
check_output(
*self.sign_cmd(path),
exception=BuildFailed(path),
cwd=os.path.dirname(path),
logger=self.logger)
return [path, f'{path}.sig']
def sign_cmd(self, path: str) -> List[str]:
cmd = ['gpg']
if self.key is not None:
cmd.extend(['-u', self.key])
cmd.extend(['-b', path])
return cmd
def sign_package(self, path: str) -> List[str]:
if self.sign != SignSettings.SignPackages:
return [path]
return self.process(path)
def sign_repository(self, path: str) -> List[str]:
if self.sign != SignSettings.SignRepository:
return [path]
return self.process(path)

54
src/ahriman/core/task.py Normal file
View File

@ -0,0 +1,54 @@
import os
import logging
import shutil
from typing import List, Optional
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildFailed
from ahriman.core.util import check_output
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class Task:
def __init__(self, package: Package, config: Configuration, paths: RepositoryPaths) -> None:
self.logger = logging.getLogger('builder')
self.build_logger = logging.getLogger('build_details')
self.package = package
self.paths = paths
self.archbuild_flags = config.get('build', 'archbuild_flags').split()
self.extra_build = config.get('build', 'extra_build')
self.makepkg_flags = config.get('build', 'makepkg_flags').split()
self.multilib_build = config.get('build', 'multilib_build')
@property
def git_path(self) -> str:
return os.path.join(self.paths.sources, self.package.name)
def build(self) -> List[str]:
build_tool = self.multilib_build if self.package.is_multilib else self.extra_build
cmd = [build_tool, '-r', self.paths.chroot]
cmd.extend(self.archbuild_flags)
if self.makepkg_flags:
cmd.extend(['--', '--'] + self.makepkg_flags)
self.logger.info(f'using {cmd} for {self.package.name}')
check_output(
*cmd,
exception=BuildFailed(self.package.name),
cwd=self.git_path,
logger=self.build_logger)
# well it is not actually correct, but we can deal with it
return check_output('makepkg', '--packagelist',
exception=BuildFailed(self.package.name),
cwd=self.git_path).splitlines()
def fetch(self, path: Optional[str] = None) -> None:
git_path = path or self.git_path
shutil.rmtree(git_path, ignore_errors=True)
check_output('git', 'clone', self.package.url, git_path, exception=None)

20
src/ahriman/core/util.py Normal file
View File

@ -0,0 +1,20 @@
import subprocess
from logging import Logger
from typing import Optional
def check_output(*args: str, exception: Optional[Exception],
cwd = None, stderr: int = subprocess.STDOUT,
logger: Optional[Logger] = None) -> str:
try:
result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode('utf8').strip()
if logger is not None:
for line in result.splitlines():
logger.debug(line)
except subprocess.CalledProcessError as e:
if e.output is not None and logger is not None:
for line in e.output.decode('utf8').splitlines():
logger.debug(line)
raise exception or e
return result

View File

View File

@ -0,0 +1,65 @@
from __future__ import annotations
import aur
import os
from configparser import RawConfigParser
from dataclasses import dataclass
from srcinfo.parse import parse_srcinfo
from typing import Type
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output
@dataclass
class Package:
name: str
version: str
url: str
@property
def is_multilib(self) -> bool:
return self.name.startswith('lib32-')
@classmethod
def from_archive(cls: Type[Package], path: str, aur_url: str) -> Package:
name, version = check_output('expac', '-p', '%n %v', path, exception=None).split()
return cls(name, version, f'{aur_url}/{name}.git')
@classmethod
def from_aur(cls: Type[Package], name: str, aur_url: str)-> Package:
package = aur.info(name)
return cls(package.name, package.version, f'{aur_url}/{name}.git')
@classmethod
def from_build(cls: Type[Package], path: str) -> Package:
git_config = RawConfigParser()
git_config.read(os.path.join(path, '.git', 'config'))
with open(os.path.join(path, '.SRCINFO')) as fn:
src_info, errors = parse_srcinfo(fn.read())
if errors:
raise InvalidPackageInfo(errors)
return cls(src_info['pkgbase'], f'{src_info["pkgver"]}-{src_info["pkgrel"]}',
git_config.get('remote "origin"', 'url'))
@classmethod
def load(cls: Type[Package], path: str, aur_url: str) -> Package:
try:
if os.path.isdir(path):
package: Package = cls.from_build(path)
elif os.path.exists(path):
package = cls.from_archive(path, aur_url)
else:
package = cls.from_aur(path, aur_url)
return package
except InvalidPackageInfo:
raise
except Exception as e:
raise InvalidPackageInfo(str(e))
def is_outdated(self, remote: Package) -> bool:
result = check_output('vercmp', self.version, remote.version, exception=None)
return True if int(result) < 0 else False

View File

@ -0,0 +1,35 @@
import os
from dataclasses import dataclass
@dataclass
class RepositoryPaths:
root: str
@property
def chroot(self) -> str:
return os.path.join(self.root, 'chroot')
@property
def manual(self) -> str:
return os.path.join(self.root, 'manual')
@property
def packages(self) -> str:
return os.path.join(self.root, 'packages')
@property
def repository(self) -> str:
return os.path.join(self.root, 'repository')
@property
def sources(self) -> str:
return os.path.join(self.root, 'sources')
def create_tree(self) -> None:
os.makedirs(self.chroot, mode=0o755, exist_ok=True)
os.makedirs(self.manual, mode=0o755, exist_ok=True)
os.makedirs(self.packages, mode=0o755, exist_ok=True)
os.makedirs(self.repository, mode=0o755, exist_ok=True)
os.makedirs(self.sources, mode=0o755, exist_ok=True)

View File

@ -0,0 +1,22 @@
from __future__ import annotations
from enum import Enum, auto
from typing import Type
from ahriman.core.exceptions import InvalidOptionException
class SignSettings(Enum):
Disabled = auto()
SignPackages = auto()
SignRepository = auto()
@classmethod
def from_option(cls: Type[SignSettings], value: str) -> SignSettings:
if value.lower() in ('no', 'disabled'):
return cls.Disabled
elif value.lower() in ('package', 'packages', 'sign-package'):
return cls.SignPackages
elif value.lower() in ('repository', 'sign-repository'):
return cls.SignRepository
raise InvalidOptionException(value)

1
src/ahriman/version.py Normal file
View File

@ -0,0 +1 @@
__version__ = '0.1.0'