Add ability to show more info in search and status subcommands

This feature also introduces the followiing changes
* aur-search command now works as expected with multiterms
* printer classes for managing of data print
* --sort-by argument for aur-search subcommand instead of using package
  name
* --quiet argument now has also --no-quite option
* if --quite is supplied, the log level will be set to warn instead of
  critical to be able to see error messages
* pretty_datetime function now also supports datetime objects
* BuildStatus is now pure dataclass
This commit is contained in:
2021-10-26 03:56:55 +03:00
parent 7351e20104
commit 09b0f2914d
38 changed files with 730 additions and 112 deletions

View File

@ -60,7 +60,8 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("-l", "--lock", help="lock file", type=Path,
default=Path(tempfile.gettempdir()) / "ahriman.lock")
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
parser.add_argument("-q", "--quiet", help="force disable any logging", action=argparse.BooleanOptionalAction,
default=False) # sometimes we would like to run not quiet even if it is disabled by default
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true")
parser.add_argument("-v", "--version", action="version", version=version.__version__)
@ -104,7 +105,12 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
description="search for package in AUR using API", formatter_class=_formatter)
parser.add_argument("search", help="search terms, can be specified multiple times", nargs="+")
parser.add_argument("search", help="search terms, can be specified multiple times, result will match all terms",
nargs="+")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
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, no_report=True, quiet=True, unsafe=True)
return parser
@ -181,6 +187,7 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser
formatter_class=_formatter)
parser.add_argument("package", help="filter status by package base", nargs="*")
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=BuildStatusEnum)
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True)
@ -354,6 +361,7 @@ def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentP
description="remove packages which are missing in AUR and do not have local PKGBUILDs",
formatter_class=_formatter)
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.set_defaults(handler=handlers.RemoveUnknown)
return parser

View File

@ -0,0 +1,19 @@
#
# 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/>.
#

View File

@ -0,0 +1,62 @@
#
# 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/>.
#
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.property import Property
class AurPrinter(Printer):
"""
print content of the AUR package
"""
def __init__(self, package: aur.Package) -> None:
"""
default constructor
:param package: AUR package description
"""
self.content = package
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
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("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
]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return f"{self.content.name} {self.content.version} ({self.content.num_votes})"

View File

@ -0,0 +1,55 @@
#
# 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 Dict, List, Optional
from ahriman.application.formatters.printer import Printer
from ahriman.models.property import Property
class ConfigurationPrinter(Printer):
"""
print content of the configuration section
"""
def __init__(self, section: str, values: Dict[str, str]) -> None:
"""
default constructor
:param section: section name
:param values: configuration values dictionary
"""
self.section = section
self.content = values
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [
Property(key, value, is_required=True)
for key, value in sorted(self.content.items())
]
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""
return f"[{self.section}]"

View File

@ -0,0 +1,60 @@
#
# 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.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.property import Property
class PackagePrinter(Printer):
"""
print content of the internal package object
"""
def __init__(self, package: Package, status: BuildStatus) -> None:
"""
default constructor
:param package: package description
:param status: build status
"""
self.content = package
self.status = status
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [
Property("Version", self.content.version, is_required=True),
Property("Groups", " ".join(self.content.groups)),
Property("Licenses", " ".join(self.content.licenses)),
Property("Depends", " ".join(self.content.depends)),
Property("Status", self.status.pretty_print(), 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.pretty_print()

View File

@ -0,0 +1,55 @@
#
# 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 Callable, List, Optional
from ahriman.models.property import Property
class Printer:
"""
base class for formatters
"""
def print(self, verbose: bool, log_fn: Callable[[str], None] = print, separator: str = ": ") -> None:
"""
print content
:param verbose: print all fields
:param log_fn: logger function to log data
:param separator: separator for property name and property value
"""
if (title := self.title()) is not None:
log_fn(title)
for prop in self.properties():
if not verbose and not prop.is_required:
continue
log_fn(f"\t{prop.name}{separator}{prop.value}")
def properties(self) -> List[Property]: # pylint: disable=no-self-use
"""
convert content into printable data
:return: list of content properties
"""
return []
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
"""

View File

@ -0,0 +1,51 @@
#
# 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.build_status import BuildStatus
from ahriman.models.property import Property
class StatusPrinter(Printer):
"""
print content of the status object
"""
def __init__(self, status: BuildStatus) -> None:
"""
default constructor
:param status: build status
"""
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
:return: content title if it can be generated and None otherwise
"""
return self.content.pretty_print()

View File

@ -21,6 +21,7 @@ import argparse
from typing import Type
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
@ -32,8 +33,6 @@ class Dump(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False
_print = print
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
@ -46,7 +45,4 @@ class Dump(Handler):
"""
dump = configuration.dump()
for section, values in sorted(dump.items()):
Dump._print(f"[{section}]")
for key, value in sorted(values.items()):
Dump._print(f"{key} = {value}")
Dump._print()
ConfigurationPrinter(section, values).print(verbose=False, separator=" = ")

View File

@ -22,9 +22,10 @@ import argparse
from typing import Type
from ahriman.application.application import Application
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
from ahriman.models.build_status import BuildStatus
class RemoveUnknown(Handler):
@ -46,16 +47,7 @@ class RemoveUnknown(Handler):
unknown_packages = application.unknown()
if args.dry_run:
for package in unknown_packages:
RemoveUnknown.log_fn(package)
PackagePrinter(package, BuildStatus()).print(args.info)
return
application.remove(package.base for package in unknown_packages)
@staticmethod
def log_fn(package: Package) -> None:
"""
log package information
:param package: package object to log
"""
print(f"=> {package.base} {package.version}")
print(f" {package.web_url}")

View File

@ -20,10 +20,12 @@
import argparse
import aur # type: ignore
from typing import Callable, Type
from typing import Callable, Dict, Iterable, List, Tuple, Type
from ahriman.application.formatters.aur_printer import AurPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidOption
class Search(Handler):
@ -32,6 +34,7 @@ class Search(Handler):
"""
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)
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
@ -43,20 +46,32 @@ class Search(Handler):
:param configuration: configuration instance
:param no_report: force disable reporting
"""
search = " ".join(args.search)
packages = aur.search(search)
packages: Dict[str, aur.Package] = {}
# see https://bugs.archlinux.org/task/49133
for search in args.search:
portion = aur.search(search)
packages = {
package.package_base: package
for package in portion
if package.package_base in packages or not packages
}
# it actually always should return string
# explicit cast to string just to avoid mypy warning for untyped library
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
for package in sorted(packages, key=comparator):
Search.log_fn(package)
packages_list = list(packages.values()) # explicit conversion for the tests
for package in Search.sort(packages_list, args.sort_by):
AurPrinter(package).print(args.info)
@staticmethod
def log_fn(package: aur.Package) -> None:
def sort(packages: Iterable[aur.Package], sort_by: str) -> List[aur.Package]:
"""
log package information
:param package: package object as from AUR
sort package list by specified field
:param packages: packages list to sort
:param sort_by: AUR package field name to sort by
:return: sorted list for packages
"""
print(f"=> {package.package_base} {package.version}")
print(f" {package.description}")
if sort_by not in Search.SORT_FIELDS:
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]] =\
lambda package: (getattr(package, sort_by), package.name)
return sorted(packages, key=comparator)

View File

@ -22,6 +22,8 @@ import argparse
from typing import Callable, Iterable, Tuple, Type
from ahriman.application.application import Application
from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.formatters.status_printer import StatusPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
@ -49,8 +51,7 @@ class Status(Handler):
client = Application(architecture, configuration, no_report=False).repository.reporter
if args.ahriman:
ahriman = client.get_self()
print(ahriman.pretty_print())
print()
StatusPrinter(ahriman).print(args.info)
if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
[client.get(base) for base in args.package],
@ -62,6 +63,4 @@ class Status(Handler):
filter_fn: Callable[[Tuple[Package, BuildStatus]], bool] =\
lambda item: args.status is None or item[1].status == args.status
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
print(package.pretty_print())
print(f"\t{package.version}")
print(f"\t{package_status.pretty_print()}")
PackagePrinter(package, package_status).print(args.info)

View File

@ -113,8 +113,7 @@ class User(Handler):
:param salt_length: salt length
:return: current salt
"""
salt = configuration.get("auth", "salt", fallback=None)
if salt:
if salt := configuration.get("auth", "salt", fallback=None):
return salt
return MUser.generate_password(salt_length)

View File

@ -175,7 +175,7 @@ class Configuration(configparser.RawConfigParser):
level=self.DEFAULT_LOG_LEVEL)
logging.exception("could not load logging from configuration, fallback to stderr")
if quiet:
logging.disable()
logging.disable(logging.WARNING) # only print errors here
def merge_sections(self, architecture: str) -> None:
"""

View File

@ -138,17 +138,17 @@ class Executor(Cleaner):
:param packages: list of filenames to run
:return: path to repository database
"""
def update_single(fn: Optional[str], base: str) -> None:
if fn is None:
def update_single(name: Optional[str], base: str) -> None:
if name is None:
self.logger.warning("received empty package name for base %s", base)
return # suppress type checking, it never can be none actually
# in theory it might be NOT packages directory, but we suppose it is
full_path = self.paths.packages / fn
full_path = self.paths.packages / name
files = self.sign.process_sign_package(full_path, base)
for src in files:
dst = self.paths.repository / src.name
shutil.move(src, dst)
package_path = self.paths.repository / fn
package_path = self.paths.repository / name
self.repo.add(package_path)
# we are iterating over bases, not single packages

View File

@ -72,16 +72,16 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
known_bases = {package.base for package in self.packages()}
for fn in self.paths.manual.iterdir():
for filename in self.paths.manual.iterdir():
try:
local = Package.load(fn, self.pacman, self.aur_url)
local = Package.load(filename, 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", fn)
self.logger.exception("could not add package from %s", filename)
self.clear_manual()
return result

View File

@ -126,11 +126,10 @@ class Watcher:
"""
for package in self.repository.packages():
# get status of build or assign unknown
current = self.known.get(package.base)
if current is None:
status = BuildStatus()
else:
if (current := self.known.get(package.base)) is not None:
_, status = current
else:
status = BuildStatus()
self.known[package.base] = (package, status)
self._cache_load()

View File

@ -24,7 +24,7 @@ import requests
from logging import Logger
from pathlib import Path
from typing import Generator, Optional, Union
from typing import Any, Dict, Generator, Iterable, Optional, Union
from ahriman.core.exceptions import InvalidOption, UnsafeRun
@ -78,6 +78,16 @@ def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
return result
def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str, Any]:
"""
filter json object by fields used for json-to-object conversion
:param source: raw json object
:param known_fields: list of fields which have to be known for the target object
:return: json object without unknown and empty fields
"""
return {key: value for key, value in source.items() if key in known_fields and value is not None}
def package_like(filename: Path) -> bool:
"""
check if file looks like package
@ -88,13 +98,17 @@ def package_like(filename: Path) -> bool:
return ".pkg." in name and not name.endswith(".sig")
def pretty_datetime(timestamp: Optional[Union[float, int]]) -> str:
def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
"""
convert datetime object to string
:param timestamp: datetime to convert
:return: pretty printable datetime as string
"""
return "" if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
if timestamp is None:
return ""
if isinstance(timestamp, (int, float)):
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
def pretty_size(size: Optional[float], level: int = 0) -> str:

View File

@ -21,10 +21,11 @@ from __future__ import annotations
import datetime
from dataclasses import dataclass, fields
from enum import Enum
from typing import Any, Dict, Optional, Type, Union
from typing import Any, Dict, Type
from ahriman.core.util import pretty_datetime
from ahriman.core.util import filter_json, pretty_datetime
class BuildStatusEnum(Enum):
@ -74,6 +75,7 @@ class BuildStatusEnum(Enum):
return "secondary"
@dataclass
class BuildStatus:
"""
build status holder
@ -81,15 +83,14 @@ class BuildStatus:
:ivar timestamp: build status update time
"""
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
timestamp: Optional[int] = None) -> None:
status: BuildStatusEnum = BuildStatusEnum.Unknown
timestamp: int = int(datetime.datetime.utcnow().timestamp())
def __post_init__(self) -> None:
"""
default constructor
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
:param timestamp: build status timestamp. Current timestamp will be used if not set
convert status to enum type
"""
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
self.status = BuildStatusEnum(self.status)
@classmethod
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
@ -98,7 +99,8 @@ class BuildStatus:
:param dump: json dump body
:return: status properties
"""
return cls(dump.get("status"), dump.get("timestamp"))
known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields))
def pretty_print(self) -> str:
"""
@ -116,20 +118,3 @@ class BuildStatus:
"status": self.status.value,
"timestamp": self.timestamp
}
def __eq__(self, other: Any) -> bool:
"""
compare object to other
:param other: other object to compare
:return: True in case if objects are equal
"""
if not isinstance(other, BuildStatus):
return False
return self.status == other.status and self.timestamp == other.timestamp
def __repr__(self) -> str:
"""
generate string representation of object
:return: unique string representation
"""
return f"BuildStatus(status={self.status.value}, timestamp={self.timestamp})"

View File

@ -22,6 +22,7 @@ from __future__ import annotations
from dataclasses import dataclass, fields
from typing import Any, Dict, List, Tuple, Type
from ahriman.core.util import filter_json
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@ -54,8 +55,7 @@ class Counters:
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
dump = {key: value for key, value in dump.items() if key in known_fields}
return cls(**dump)
return cls(**filter_json(dump, known_fields))
@classmethod
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:

View File

@ -24,6 +24,8 @@ from pathlib import Path
from pyalpm import Package # type: ignore
from typing import Any, Dict, List, Optional, Type
from ahriman.core.util import filter_json
@dataclass
class PackageDescription:
@ -70,8 +72,7 @@ class PackageDescription:
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
dump = {key: value for key, value in dump.items() if key in known_fields}
return cls(**dump)
return cls(**filter_json(dump, known_fields))
@classmethod
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:

View File

@ -0,0 +1,31 @@
# (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 dataclasses import dataclass
from typing import Any
@dataclass
class Property:
"""
holder of object properties descriptor
:ivar name: name of the property
:ivar value: property value
:ivar is_required: if set to True then this property is required
"""
name: str
value: Any
is_required: bool = False

View File

@ -81,8 +81,7 @@ def auth_handler() -> MiddlewareType:
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
permission_method = getattr(handler, "get_permission", None)
if permission_method is not None:
if (permission_method := getattr(handler, "get_permission", None)) is not None:
permission = await permission_method(request)
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
handler_instance = getattr(handler, "__self__", None)

View File

@ -45,11 +45,11 @@ class LoginView(BaseView):
"""
from ahriman.core.auth.oauth import OAuth
code = self.request.query.getone("code", default=None)
oauth_provider = self.validator
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
code = self.request.query.getone("code", default=None)
if not code:
raise HTTPFound(oauth_provider.get_oauth_url())