Docstring update (#58)

* migrate docstrings from reST to google format

* add raises note

Also change behaviour of the `from_option` method to fallback to
disabled instead of raising exception on unknown option

* fix part of warnings for sphinx

* make identation a bit more readable

* review fixes

* add verbose description for properties to make them parsed by sphinx extenstion

* add demo sphinx generator
This commit is contained in:
2022-04-17 20:25:28 +03:00
committed by GitHub
parent 0db619136d
commit d90f417cae
203 changed files with 5246 additions and 1636 deletions

View File

@ -39,21 +39,29 @@ from ahriman.web.middlewares import HandlerType, MiddlewareType
class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type: ignore
"""
authorization policy implementation
:ivar validator: validator instance
Attributes:
validator(Auth): validator instance
"""
def __init__(self, validator: Auth) -> None:
"""
default constructor
:param validator: authorization module instance
Args:
validator(Auth): authorization module instance
"""
self.validator = validator
async def authorized_userid(self, identity: str) -> Optional[str]:
"""
retrieve authenticated username
:param identity: username
:return: user identity (username) in case if user exists and None otherwise
Args:
identity(str): username
Returns:
Optional[str]: user identity (username) in case if user exists and None otherwise
"""
user = UserIdentity.from_identity(identity)
if user is None:
@ -63,10 +71,14 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
async def permits(self, identity: str, permission: UserAccess, context: Optional[str] = None) -> bool:
"""
check user permissions
:param identity: username
:param permission: requested permission level
:param context: URI request path
:return: True in case if user is allowed to perform this request and False otherwise
Args:
identity(str): username
permission(UserAccess): requested permission level
context(Optional[str], optional): URI request path (Default value = None)
Returns:
bool: True in case if user is allowed to perform this request and False otherwise
"""
user = UserIdentity.from_identity(identity)
if user is None:
@ -77,7 +89,9 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
def auth_handler() -> MiddlewareType:
"""
authorization and authentication middleware
:return: built middleware
Returns:
MiddlewareType: built middleware
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
@ -99,9 +113,13 @@ def auth_handler() -> MiddlewareType:
def setup_auth(application: web.Application, validator: Auth) -> web.Application:
"""
setup authorization policies for the application
:param application: web application instance
:param validator: authorization module instance
:return: configured web application
Args:
application(web.Application): web application instance
validator(Auth): authorization module instance
Returns:
web.Application: configured web application
"""
fernet_key = fernet.Fernet.generate_key()
secret_key = base64.urlsafe_b64decode(fernet_key)

View File

@ -28,8 +28,12 @@ from ahriman.web.middlewares import HandlerType, MiddlewareType
def exception_handler(logger: Logger) -> MiddlewareType:
"""
exception handler middleware. Just log any exception (except for client ones)
:param logger: class logger
:return: built middleware
Args:
logger(Logger): class logger
Returns:
MiddlewareType: built middleware
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
@ -40,8 +44,8 @@ def exception_handler(logger: Logger) -> MiddlewareType:
except HTTPServerError as e:
logger.exception("server exception during performing request to %s", request.path)
return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPException:
raise # just raise 2xx and 3xx codes
except HTTPException: # just raise 2xx and 3xx codes
raise
except Exception as e:
logger.exception("unknown exception during performing request to %s", request.path)
return json_response(data={"error": str(e)}, status=500)

View File

@ -39,37 +39,38 @@ def setup_routes(application: Application, static_path: Path) -> None:
Available routes are:
GET / get build status page
GET /index.html same as above
* GET / get build status page
* GET /index.html same as above
POST /service-api/v1/add add new packages to repository
* POST /service-api/v1/add add new packages to repository
POST /service-api/v1/remove remove existing package from repository
* POST /service-api/v1/remove remove existing package from repository
POST /service-api/v1/request request to add new packages to repository
* POST /service-api/v1/request request to add new packages to repository
GET /service-api/v1/search search for substring in AUR
* GET /service-api/v1/search search for substring in AUR
POST /service-api/v1/update update packages in repository, actually it is just alias for add
* POST /service-api/v1/update update packages in repository, actually it is just alias for add
GET /status-api/v1/ahriman get current service status
POST /status-api/v1/ahriman update service status
* GET /status-api/v1/ahriman get current service status
* POST /status-api/v1/ahriman update service status
GET /status-api/v1/packages get all known packages
POST /status-api/v1/packages force update every package from repository
* GET /status-api/v1/packages get all known packages
* POST /status-api/v1/packages force update every package from repository
DELETE /status-api/v1/package/:base delete package base from status page
GET /status-api/v1/package/:base get package base status
POST /status-api/v1/package/:base update package base status
* DELETE /status-api/v1/package/:base delete package base from status page
* GET /status-api/v1/package/:base get package base status
* POST /status-api/v1/package/:base update package base status
GET /status-api/v1/status get web service status itself
* GET /status-api/v1/status get web service status itself
GET /user-api/v1/login OAuth2 handler for login
POST /user-api/v1/login login to service
POST /user-api/v1/logout logout from service
* GET /user-api/v1/login OAuth2 handler for login
* POST /user-api/v1/login login to service
* POST /user-api/v1/logout logout from service
:param application: web application instance
:param static_path: path to static files directory
Args:
application(Application): web application instance
static_path(Path): path to static files directory
"""
application.router.add_get("/", IndexView, allow_head=True)
application.router.add_get("/index.html", IndexView, allow_head=True)

View File

@ -38,7 +38,10 @@ class BaseView(View):
@property
def configuration(self) -> Configuration:
"""
:return: configuration instance
get configuration instance
Returns:
Configuration: configuration instance
"""
configuration: Configuration = self.request.app["configuration"]
return configuration
@ -46,7 +49,10 @@ class BaseView(View):
@property
def database(self) -> SQLite:
"""
:return: database instance
get database instance
Returns:
SQLite: database instance
"""
database: SQLite = self.request.app["database"]
return database
@ -54,7 +60,10 @@ class BaseView(View):
@property
def service(self) -> Watcher:
"""
:return: build status watcher instance
get status watcher instance
Returns:
Watcher: build status watcher instance
"""
watcher: Watcher = self.request.app["watcher"]
return watcher
@ -62,7 +71,10 @@ class BaseView(View):
@property
def spawner(self) -> Spawn:
"""
:return: external process spawner instance
get process spawner instance
Returns:
Spawn: external process spawner instance
"""
spawner: Spawn = self.request.app["spawn"]
return spawner
@ -70,7 +82,10 @@ class BaseView(View):
@property
def validator(self) -> Auth:
"""
:return: authorization service instance
get authorization instance
Returns:
Auth: authorization service instance
"""
validator: Auth = self.request.app["validator"]
return validator
@ -79,8 +94,12 @@ class BaseView(View):
async def get_permission(cls: Type[BaseView], request: Request) -> UserAccess:
"""
retrieve user permission from the request
:param request: request object
:return: extracted permission
Args:
request(Request): request object
Returns:
UserAccess: extracted permission
"""
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Write)
return permission
@ -88,8 +107,13 @@ class BaseView(View):
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
"""
extract json data from either json or form data
:param list_keys: optional list of keys which must be forced to list from form data
:return: raw json object or form data converted to json
Args:
list_keys(Optional[List[str]], optional): optional list of keys which must be forced to list from form data
(Default value = None)
Returns:
Dict[str, Any]: raw json object or form data converted to json
"""
try:
json: Dict[str, Any] = await self.request.json()
@ -100,8 +124,13 @@ class BaseView(View):
async def data_as_json(self, list_keys: List[str]) -> Dict[str, Any]:
"""
extract form data and convert it to json object
:param list_keys: list of keys which must be forced to list from form data
:return: form data converted to json. In case if a key is found multiple times it will be returned as list
Args:
list_keys(List[str]): list of keys which must be forced to list from form data
Returns:
Dict[str, Any]: form data converted to json. In case if a key is found multiple times
it will be returned as list
"""
raw = await self.request.post()
json: Dict[str, Any] = {}

View File

@ -31,35 +31,37 @@ from ahriman.web.views.base import BaseView
class IndexView(BaseView):
"""
root view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
It uses jinja2 templates for report generation, the following variables are allowed:
architecture - repository architecture, string, required
auth - authorization descriptor, required
* authenticated - alias to check if user can see the page, boolean, required
* control - HTML to insert for login control, HTML string, required
* enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated
index_url - url to the repository index, string, optional
packages - sorted list of packages properties, required
* base, string
* depends, sorted list of strings
* groups, sorted list of strings
* licenses, sorted list of strings
* packages, sorted list of strings
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version, string
* web_url, string
repository - repository name, string, required
service - service status properties, required
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
version - ahriman version, string, required
* architecture - repository architecture, string, required
* auth - authorization descriptor, required
* authenticated - alias to check if user can see the page, boolean, required
* control - HTML to insert for login control, HTML string, required
* enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated
* index_url - url to the repository index, string, optional
* packages - sorted list of packages properties, required
* base, string
* depends, sorted list of strings
* groups, sorted list of strings
* licenses, sorted list of strings
* packages, sorted list of strings
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version, string
* web_url, string
* repository - repository name, string, required
* service - service status properties, required
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version - ahriman version, string, required
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Safe
@ -68,7 +70,9 @@ class IndexView(BaseView):
async def get(self) -> Dict[str, Any]:
"""
process get request. No parameters supported here
:return: parameters for jinja template
Returns:
Dict[str, Any]: parameters for jinja template
"""
# some magic to make it jinja-friendly
packages = [

View File

@ -17,7 +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 HTTPBadRequest, HTTPFound, Response
from aiohttp.web import HTTPBadRequest, HTTPFound
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -26,21 +26,26 @@ from ahriman.web.views.base import BaseView
class AddView(BaseView):
"""
add package web view
:cvar POST_PERMISSION: post permissions of self
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Write
async def post(self) -> Response:
async def post(self) -> None:
"""
add new package
JSON body must be supplied, the following model is used:
{
"packages": "ahriman" # either list of packages or package name as in AUR
}
:return: redirect to main page on success
>>> {
>>> "packages": "ahriman" # either list of packages or package name as in AUR
>>> }
Raises:
HTTPBadRequest: if bad data is supplied
HTTPFound: in case of success response
"""
try:
data = await self.extract_data(["packages"])

View File

@ -17,7 +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 HTTPBadRequest, HTTPFound, Response
from aiohttp.web import HTTPBadRequest, HTTPFound
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -26,21 +26,26 @@ from ahriman.web.views.base import BaseView
class RemoveView(BaseView):
"""
remove package web view
:cvar POST_PERMISSION: post permissions of self
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Write
async def post(self) -> Response:
async def post(self) -> None:
"""
remove existing packages
JSON body must be supplied, the following model is used:
{
"packages": "ahriman", # either list of packages or package name
}
:return: redirect to main page on success
>>> {
>>> "packages": "ahriman", # either list of packages or package name
>>> }
Raises:
HTTPBadRequest: if bad data is supplied
HTTPFound: in case of success response
"""
try:
data = await self.extract_data(["packages"])

View File

@ -17,7 +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 HTTPBadRequest, HTTPFound, Response
from aiohttp.web import HTTPBadRequest, HTTPFound
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -26,21 +26,26 @@ from ahriman.web.views.base import BaseView
class RequestView(BaseView):
"""
request package web view. It is actually the same as AddView, but without now
:cvar POST_PERMISSION: post permissions of self
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Read
async def post(self) -> Response:
async def post(self) -> None:
"""
request to add new package
JSON body must be supplied, the following model is used:
{
"packages": "ahriman" # either list of packages or package name as in AUR
}
:return: redirect to main page on success
>>> {
>>> "packages": "ahriman" # either list of packages or package name as in AUR
>>> }
Raises:
HTTPBadRequest: if bad data is supplied
HTTPFound: in case of success response
"""
try:
data = await self.extract_data(["packages"])

View File

@ -29,8 +29,10 @@ from ahriman.web.views.base import BaseView
class SearchView(BaseView):
"""
AUR search web view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
@ -41,7 +43,11 @@ class SearchView(BaseView):
search string (non empty) must be supplied as `for` parameter
:return: 200 with found package bases and descriptions sorted by base
Returns:
Response: 200 with found package bases and descriptions sorted by base
Raises:
HTTPNotFound: if no packages found
"""
search: List[str] = self.request.query.getall("for", default=[])
packages = AUR.multisearch(*search)

View File

@ -27,9 +27,11 @@ from ahriman.web.views.base import BaseView
class AhrimanView(BaseView):
"""
service status web view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
:cvar POST_PERMISSION: post permissions of self
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
@ -38,20 +40,25 @@ class AhrimanView(BaseView):
async def get(self) -> Response:
"""
get current service status
:return: 200 with service status object
Returns:
Response: 200 with service status object
"""
return json_response(self.service.status.view())
async def post(self) -> Response:
async def post(self) -> None:
"""
update service status
JSON body must be supplied, the following model is used:
{
"status": "unknown", # service status string, must be valid `BuildStatusEnum`
}
:return: 204 on success
>>> {
>>> "status": "unknown", # service status string, must be valid `BuildStatusEnum`
>>> }
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
try:
data = await self.extract_data()

View File

@ -29,10 +29,12 @@ from ahriman.web.views.base import BaseView
class PackageView(BaseView):
"""
package base specific web view
:cvar DELETE_PERMISSION: delete permissions of self
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
:cvar POST_PERMISSION: post permissions of self
Attributes:
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Write
@ -41,7 +43,12 @@ class PackageView(BaseView):
async def get(self) -> Response:
"""
get current package base status
:return: 200 with package description on success
Returns:
Response: 200 with package description on success
Raises:
HTTPNotFound: if no package was found
"""
base = self.request.match_info["package"]
@ -58,28 +65,33 @@ class PackageView(BaseView):
]
return json_response(response)
async def delete(self) -> Response:
async def delete(self) -> None:
"""
delete package base from status page
:return: 204 on success
Raises:
HTTPNoContent: on success response
"""
base = self.request.match_info["package"]
self.service.remove(base)
raise HTTPNoContent()
async def post(self) -> Response:
async def post(self) -> None:
"""
update package build status
JSON body must be supplied, the following model is used:
{
"status": "unknown", # package build status string, must be valid `BuildStatusEnum`
"package": {} # package body (use `dataclasses.asdict` to generate one), optional.
# Must be supplied in case if package base is unknown
}
:return: 204 on success
>>> {
>>> "status": "unknown", # package build status string, must be valid `BuildStatusEnum`
>>> "package": {} # package body (use `dataclasses.asdict` to generate one), optional.
>>> # Must be supplied in case if package base is unknown
>>> }
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
base = self.request.match_info["package"]
data = await self.extract_data()

View File

@ -26,9 +26,11 @@ from ahriman.web.views.base import BaseView
class PackagesView(BaseView):
"""
global watcher view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
:cvar POST_PERMISSION: post permissions of self
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
@ -37,7 +39,9 @@ class PackagesView(BaseView):
async def get(self) -> Response:
"""
get current packages status
:return: 200 with package description on success
Returns:
Response: 200 with package description on success
"""
response = [
{
@ -47,10 +51,12 @@ class PackagesView(BaseView):
]
return json_response(response)
async def post(self) -> Response:
async def post(self) -> None:
"""
reload all packages from repository. No parameters supported here
:return: 204 on success
Raises:
HTTPNoContent: on success response
"""
self.service.load()

View File

@ -29,8 +29,10 @@ from ahriman.web.views.base import BaseView
class StatusView(BaseView):
"""
web service status web view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
@ -38,7 +40,9 @@ class StatusView(BaseView):
async def get(self) -> Response:
"""
get current service status
:return: 200 with service status object
Returns:
Response: 200 with service status object
"""
counters = Counters.from_packages(self.service.packages)
status = InternalStatus(

View File

@ -17,7 +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 HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized, Response
from aiohttp.web import HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized
from ahriman.core.auth.helpers import remember
from ahriman.models.user_access import UserAccess
@ -28,20 +28,25 @@ from ahriman.web.views.base import BaseView
class LoginView(BaseView):
"""
login endpoint view
:cvar GET_PERMISSION: get permissions of self
:cvar POST_PERMISSION: post permissions of self
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = POST_PERMISSION = UserAccess.Safe
async def get(self) -> Response:
async def get(self) -> None:
"""
OAuth2 response handler
In case if code provided it will do a request to get user email. In case if no code provided it will redirect
to authorization url provided by OAuth client
:return: redirect to main page
Raises:
HTTPFound: on success response
HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled
HTTPUnauthorized: if case of authorization error
"""
from ahriman.core.auth.oauth import OAuth
@ -62,17 +67,20 @@ class LoginView(BaseView):
raise HTTPUnauthorized()
async def post(self) -> Response:
async def post(self) -> None:
"""
login user to service
either JSON body or form data must be supplied the following fields are required:
{
"username": "username" # username to use for login
"password": "pa55w0rd" # password to use for login
}
:return: redirect to main page
>>> {
>>> "username": "username" # username to use for login
>>> "password": "pa55w0rd" # password to use for login
>>> }
Raises:
HTTPFound: on success response
HTTPUnauthorized: if case of authorization error
"""
data = await self.extract_data()
username = data.get("username")

View File

@ -17,7 +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 HTTPFound, Response
from aiohttp.web import HTTPFound
from ahriman.core.auth.helpers import check_authorized, forget
from ahriman.models.user_access import UserAccess
@ -27,15 +27,19 @@ from ahriman.web.views.base import BaseView
class LogoutView(BaseView):
"""
logout endpoint view
:cvar POST_PERMISSION: post permissions of self
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Safe
async def post(self) -> Response:
async def post(self) -> None:
"""
logout user from the service. No parameters supported here
:return: redirect to main page
Raises:
HTTPFound: on success response
"""
await check_authorized(self.request)
await forget(self.request, HTTPFound("/"))

View File

@ -36,7 +36,9 @@ from ahriman.web.routes import setup_routes
async def on_shutdown(application: web.Application) -> None:
"""
web application shutdown handler
:param application: web application instance
Args:
application(web.Application): web application instance
"""
application.logger.warning("server terminated")
@ -44,7 +46,12 @@ async def on_shutdown(application: web.Application) -> None:
async def on_startup(application: web.Application) -> None:
"""
web application start handler
:param application: web application instance
Args:
application(web.Application): web application instance
Raises:
InitializeException: in case if matched could not be loaded
"""
application.logger.info("server started")
try:
@ -58,7 +65,9 @@ async def on_startup(application: web.Application) -> None:
def run_server(application: web.Application) -> None:
"""
run web application
:param application: web application instance
Args:
application(web.Application): web application instance
"""
application.logger.info("start server")
@ -73,10 +82,14 @@ def run_server(application: web.Application) -> None:
def setup_service(architecture: str, configuration: Configuration, spawner: Spawn) -> web.Application:
"""
create web application
:param architecture: repository architecture
:param configuration: configuration instance
:param spawner: spawner thread
:return: web application instance
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
spawner(Spawn): spawner thread
Returns:
web.Application: web application instance
"""
application = web.Application(logger=logging.getLogger("http"))
application.on_shutdown.append(on_shutdown)