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 3, 2023
1 parent 7f5e541 commit 5cfd8af
Show file tree
Hide file tree
Showing 97 changed files with 2,247 additions and 631 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
# VCS support
pacman --noconfirm -Sy breezy darcs mercurial subversion
# web server
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
# additional features
pacman --noconfirm -Sy gnupg python-boto3 rsync
fi
Expand Down
46 changes: 46 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,52 @@ Again, the most checks can be performed by `make check` command, though some add
* No global variable is allowed outside of `ahriman.version` module. `ahriman.core.context` is also special case.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
* Web API methods must be documented by using `aiohttp_apispec` library. Schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):

```python
import aiohttp_apispec

from marshmallow import Schema, fields

from ahriman.web.schemas.auth_schema import AuthSchema
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.package_name_schema import PackageNameSchema
from ahriman.web.views.base import BaseView


class RequestSchema(Schema):

field = fields.String(metadata={"description": "Field description", "example": "foo"})


class ResponseSchema(Schema):

field = fields.String(required=True, metadata={"description": "Field description"})


class Foo(BaseView):

POST_PERMISSION = ...

@aiohttp_apispec.docs(
tags=["Tag"],
summary="Do foo",
description="Extended description of the method which does foo",
responses={
200: {"description": "Success response", "schema": ResponseSchema},
204: {"description": "Success response"}, # example without json schema response
400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method
401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented
403: {"description": "Access is forbidden", "schema": ErrorSchema}, # should be always presented
500: {"description": "Internal server error", "schema": ErrorSchema}, # should be always presented
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema) # should be always presented
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.json_schema(RequestSchema(many=True))
async def post(self) -> None: ...
```

### Other checks

Expand Down
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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ The application provides reasonable defaults which allow to use it out-of-box; h

## Live demos

* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks.
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks. [HTTP API documentation](https://ahriman-demo.arcanis.me/api-docs) is also available.
* [Repository index](http://repo.arcanis.me/x86_64/index.html).
* [Telegram feed](https://t.me/arcanisrepo).
5 changes: 5 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,11 @@ How to enable OAuth authorization
#.
Restart web service ``systemctl restart ahriman-web@x86_64``.

How to implement own interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can write your own interface by using API which is provided by the web service. Full autogenerated API documentation is available at ``http://localhost:8080/api-docs``.

Backup and restore
------------------

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(),
)
Loading

0 comments on commit 5cfd8af

Please sign in to comment.