Skip to content

Commit

Permalink
use api generated docs instead of comments
Browse files Browse the repository at this point in the history
  • Loading branch information
arcan1s committed Apr 2, 2023
1 parent 7f5e541 commit fde738b
Show file tree
Hide file tree
Showing 87 changed files with 2,166 additions and 568 deletions.
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
RUN pacman --noconfirm -Sy devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-setuptools python-srcinfo && \
pacman --noconfirm -Sy python-build python-installer python-wheel && \
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-jinja2 python-aiohttp-debugtoolbar \
python-aiohttp-session python-aiohttp-security
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \
python-aiohttp-debugtoolbar python-aiohttp-session python-aiohttp-security

# cleanup unused
RUN find "/var/cache/pacman/pkg" -type f -delete
Expand Down
2 changes: 2 additions & 0 deletions package/archlinux/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ optdepends=('breezy: -bzr packages support'
'mercurial: -hg packages support'
'python-aioauth-client: web server with OAuth2 authorization'
'python-aiohttp: web server'
'python-aiohttp-apispec>=3.0.0: web server'
'python-aiohttp-cors: web server'
'python-aiohttp-debugtoolbar: web server with enabled debug panel'
'python-aiohttp-jinja2: web server'
'python-aiohttp-security: web server with authorization'
Expand Down
23 changes: 23 additions & 0 deletions package/share/ahriman/templates/api.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ahriman API</title>

<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">

<link rel="shortcut icon" href="/static/favicon.ico">
</head>
<body>

<elements-api
apiDescriptionUrl="/api-docs/swagger.json"
router="hash"
layout="sidebar"
/>

</body>
</html>
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
]),
# templates
("share/ahriman/templates", [
"package/share/ahriman/templates/api.jinja2",
"package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.jinja2",
"package/share/ahriman/templates/error.jinja2",
Expand Down Expand Up @@ -140,9 +141,11 @@
],
"web": [
"Jinja2",
"aioauth-client",
"aiohttp",
"aiohttp-apispec",
"aiohttp_cors",
"aiohttp_jinja2",
"aioauth-client",
"aiohttp_debugtoolbar",
"aiohttp_session",
"aiohttp_security",
Expand Down
2 changes: 1 addition & 1 deletion src/ahriman/core/configuration/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from ahriman.core.configuration import Configuration


class Validator(RootValidator): # type: ignore
class Validator(RootValidator):
"""
class which defines custom validation methods for the service configuration
Expand Down
16 changes: 6 additions & 10 deletions src/ahriman/core/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
#
from __future__ import annotations

import itertools
import functools

from typing import Callable, Iterable, List, Tuple
from typing import Callable, Iterable, List

from ahriman.core.util import partition
from ahriman.models.package import Package


Expand Down Expand Up @@ -149,13 +150,6 @@ def levels(self) -> List[List[Package]]:
Returns:
List[List[Package]]: sorted list of packages lists based on their dependencies
"""
# https://docs.python.org/dev/library/itertools.html#itertools-recipes
def partition(source: List[Leaf]) -> Tuple[List[Leaf], Iterable[Leaf]]:
first_iter, second_iter = itertools.tee(source)
filter_fn: Callable[[Leaf], bool] = lambda leaf: leaf.is_dependency(next_level)
# materialize first list and leave second as iterator
return list(filter(filter_fn, first_iter)), itertools.filterfalse(filter_fn, second_iter)

unsorted: List[List[Leaf]] = []

# build initial tree
Expand All @@ -170,7 +164,9 @@ def partition(source: List[Leaf]) -> Tuple[List[Leaf], Iterable[Leaf]]:
next_level = unsorted[next_num]

# change lists inside the collection
unsorted[current_num], to_be_moved = partition(current_level)
# additional workaround with partial in order to hide cell-var-from-loop pylint warning
predicate = functools.partial(Leaf.is_dependency, packages=next_level)
unsorted[current_num], to_be_moved = partition(current_level, predicate)
unsorted[next_num].extend(to_be_moved)

comparator: Callable[[Package], str] = lambda package: package.base
Expand Down
39 changes: 36 additions & 3 deletions src/ahriman/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#
import datetime
import io
import itertools
import logging
import os
import re
Expand All @@ -28,14 +29,31 @@
from enum import Enum
from pathlib import Path
from pwd import getpwuid
from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union
from typing import Any, Callable, Dict, Generator, IO, Iterable, List, Optional, Type, TypeVar, Tuple, Union

from ahriman.core.exceptions import OptionError, UnsafeRunError
from ahriman.models.repository_paths import RepositoryPaths


__all__ = ["check_output", "check_user", "enum_values", "exception_response_text", "filter_json", "full_version",
"package_like", "pretty_datetime", "pretty_size", "safe_filename", "trim_package", "utcnow", "walk"]
__all__ = [
"check_output",
"check_user",
"enum_values",
"exception_response_text",
"filter_json",
"full_version",
"package_like",
"partition",
"pretty_datetime",
"pretty_size",
"safe_filename",
"trim_package",
"utcnow",
"walk",
]


T = TypeVar("T")


def check_output(*args: str, exception: Optional[Exception] = None, cwd: Optional[Path] = None,
Expand Down Expand Up @@ -225,6 +243,21 @@ def package_like(filename: Path) -> bool:
return ".pkg." in name and not name.endswith(".sig")


def partition(source: List[T], predicate: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
"""
partition list into two based on predicate, based on # https://docs.python.org/dev/library/itertools.html#itertools-recipes
Args:
source(List[T]): source list to be partitioned
predicate(Callable[[T], bool]): filter function
Returns:
Tuple[List[T], List[T]]: two lists, first is which ``predicate`` is ``True``, second is ``False``
"""
first_iter, second_iter = itertools.tee(source)
return list(filter(predicate, first_iter)), list(itertools.filterfalse(predicate, second_iter))


def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
"""
convert datetime object to string
Expand Down
120 changes: 120 additions & 0 deletions src/ahriman/web/apispec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#
# Copyright (c) 2021-2023 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 aiohttp_apispec # type: ignore

from aiohttp import web
from typing import Any, Dict, List

from ahriman import version
from ahriman.core.configuration import Configuration


__all__ = ["setup_apispec"]


def _info() -> Dict[str, Any]:
"""
create info object for swagger docs
Returns:
Dict[str, Any]: info object as per openapi specification
"""
return {
"title": "ahriman",
"description": """Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
## Features
* Install-configure-forget manager for the very own repository.
* Multi-architecture support.
* Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Sign support with gpg (repository, package, per package settings).
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options
<security-definitions />
""",
"license": {
"name": "GPL3",
"url": "https://raw.githubusercontent.com/arcan1s/ahriman/master/LICENSE",
},
"version": version.__version__,
}


def _security() -> List[Dict[str, Any]]:
"""
get security definitions
Returns:
List[Dict[str, Any]]: generated security definition
"""
return [{
"token": {
"type": "apiKey", # as per specification we are using api key
"name": "API_SESSION",
"in": "cookie",
}
}]


def _servers(application: web.Application) -> List[Dict[str, Any]]:
"""
get list of defined addresses for server
Args:
application(web.Application): web application instance
Returns:
List[Dict[str, Any]]: list (actually only one) of defined web urls
"""
configuration: Configuration = application["configuration"]
address = configuration.get("web", "address", fallback=None)
if not address:
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
address = f"http://{host}:{port}"

return [{
"url": address,
}]


def setup_apispec(application: web.Application) -> aiohttp_apispec.AiohttpApiSpec:
"""
setup swagger api specification
Args:
application(web.Application): web application instance
Returns:
aiohttp_apispec.AiohttpApiSpec: created specification instance
"""
return aiohttp_apispec.setup_aiohttp_apispec(
application,
url="/api-docs/swagger.json",
openapi_version="3.0.2",
info=_info(),
servers=_servers(application),
security=_security(),
)
48 changes: 48 additions & 0 deletions src/ahriman/web/cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#
# Copyright (c) 2021-2023 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 aiohttp_cors # type: ignore

from aiohttp import web


__all__ = ["setup_cors"]


def setup_cors(application: web.Application) -> aiohttp_cors.CorsConfig:
"""
setup CORS for the web application
Args:
application(web.Application): web application instance
Returns:
aiohttp_cors.CorsConfig: generated CORS configuration
"""
cors = aiohttp_cors.setup(application, defaults={
"*": aiohttp_cors.ResourceOptions(
expose_headers="*",
allow_headers="*",
allow_methods="*",
)
})
for route in application.router.routes():
cors.add(route)

return cors
3 changes: 1 addition & 2 deletions src/ahriman/web/middlewares/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
# 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 aiohttp.web import Request
from aiohttp.web_response import StreamResponse
from aiohttp.web import Request, StreamResponse
from typing import Awaitable, Callable


Expand Down
2 changes: 1 addition & 1 deletion src/ahriman/web/middlewares/auth_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
__all__ = ["AuthorizationPolicy", "auth_handler", "cookie_secret_key", "setup_auth"]


class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type: ignore
class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
"""
authorization policy implementation
Expand Down
15 changes: 13 additions & 2 deletions src/ahriman/web/middlewares/exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
import aiohttp_jinja2
import logging

from aiohttp.web import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized, Request, StreamResponse, \
json_response, middleware
from aiohttp.web import HTTPClientError, HTTPException, HTTPMethodNotAllowed, HTTPNoContent, HTTPServerError, \
HTTPUnauthorized, Request, StreamResponse, json_response, middleware

from ahriman.web.middlewares import HandlerType, MiddlewareType

Expand All @@ -48,6 +48,17 @@ async def handle(request: Request, handler: HandlerType) -> StreamResponse:
context = {"code": e.status_code, "reason": e.reason}
return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code)
return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPMethodNotAllowed as e:
if e.method == "OPTIONS":
# automatically handle OPTIONS method, idea comes from
# https://github.com/arcan1s/ffxivbis/blob/master/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala#L32
raise HTTPNoContent(headers={"Allow": ",".join(sorted(e.allowed_methods))})
if e.method == "HEAD":
# since we have special autogenerated HEAD method, we need to remove it from list of available
e.allowed_methods = {method for method in e.allowed_methods if method != "HEAD"}
e.headers["Allow"] = ",".join(sorted(e.allowed_methods))
raise e
raise
except HTTPClientError as e:
return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPServerError as e:
Expand Down
Loading

0 comments on commit fde738b

Please sign in to comment.