Skip to content

Commit

Permalink
Documenting
Browse files Browse the repository at this point in the history
  • Loading branch information
Etienne Jodry authored and Etienne Jodry committed May 3, 2024
1 parent 7b3961a commit c7956a4
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 39 deletions.
15 changes: 9 additions & 6 deletions src/biodm/basics/k8scontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from copy import deepcopy

from starlette.routing import Mount, Route
from starlette.requests import Request
from starlette.responses import Response

from biodm.components.controllers import Controller, HttpMethod
Expand All @@ -18,8 +19,11 @@ def prefix(self):
return "/k8s_instances"

def routes(self, schema=False) -> Mount:
"""
schema:
"""Routes for k8s instances management
:param schema: when called from schema generation, defaults to False
:type schema: bool
:rtype: starlette.routing.Mount
"""
m = Mount(self.prefix, routes=[
Route( "/{manifest}", self.create, methods=[HttpMethod.POST.value]),
Expand All @@ -36,7 +40,6 @@ def routes(self, schema=False) -> Mount:
for key in keys:
r_view = deepcopy(r_create)
r_view.path = f"/{key}"
# dummy = function()
def dummy():
""""""
dummy.__doc__ = mans.__dict__[key].__doc__
Expand All @@ -48,12 +51,12 @@ def dummy():
def k8s(self):
return self.app.k8s

async def list_instances(self, request) -> Response:
async def list_instances(self, request: Request) -> Response:
"""
"""
return '{}'

async def create(self, request) -> Response:
async def create(self, request: Request) -> Response:
"""
---
description: Deploys manifest matching identifier and tie it to the user
Expand All @@ -69,7 +72,7 @@ async def create(self, request) -> Response:
return self.svc.create(self.k8s.manifests.__dict__[manifest])
raise ManifestError

async def instance_info(self, request) -> Response:
async def instance_info(self, request: Request) -> Response:
"""
"""
return '{}'
6 changes: 5 additions & 1 deletion src/biodm/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from biodm.api import Api

class ApiComponent(ABC):
"""Abstract API component, refrencing main server class and its loggger."""
"""Abstract API component, refrencing main server class and its loggger.
:param app: Reference to running server class.
:type app: class:`biodm.Api`
"""
app: Api
logger: logging.logger

Expand Down
31 changes: 25 additions & 6 deletions src/biodm/components/controllers/controller.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations
import io
import json
from abc import abstractmethod
from enum import Enum
from typing import Any
from typing import Any, List, TYPE_CHECKING

from marshmallow.schema import Schema, EXCLUDE, INCLUDE
from marshmallow.exceptions import ValidationError
Expand All @@ -12,6 +13,10 @@
from biodm.exceptions import PayloadJSONDecodingError, PayloadValidationError, AsyncDBError
from biodm.utils.utils import json_response

if TYPE_CHECKING:
from biodm.component import Base



class HttpMethod(Enum):
"""HTTP Methods."""
Expand All @@ -38,7 +43,8 @@ def schema_gen(self):
return self.app.schema_generator

async def openapi_schema(self, _):
"""
""" Generates openapi schema for this controllers' routes.
Relevant Documentation:
- starlette: https://www.starlette.io/schemas/
- doctrings: https://apispec.readthedocs.io/en/stable/
Expand All @@ -63,12 +69,19 @@ async def openapi_schema(self, _):
class EntityController(Controller, CRUDApiComponent):
"""EntityController - A controller performing validation and serialization given a schema.
Also requires CRUD methods implementation for that entity.
:param schema: Entity schema class
:type schema: class:`marshmallow.schema.Schema`
"""
schema: Schema

@classmethod
def deserialize(cls, data: Any) -> (Any | list | dict | None):
"""Deserialize."""
def validate(cls, data: bytes) -> (Any | list | dict | None):
"""Checks incoming data against class schema and marshall to python dict.
:param data: some request body
:type data: bytes
"""
try:
json_data = json.load(io.BytesIO(data))
cls.schema.many = isinstance(json_data, list)
Expand All @@ -82,8 +95,14 @@ def deserialize(cls, data: Any) -> (Any | list | dict | None):
raise e

@classmethod
def serialize(cls, data: Any, many: bool) -> (str | Any):
"""Serialize."""
def serialize(cls, data: dict | Base | List[Base], many: bool) -> str:
"""Serialize SQLAlchemy statement execution result to json.
:param data: some request body
:type data: dict, class:`biodm.components.Base`, List[class:`biodm.components.Base`]
:param many: plurality flag, essential to marshmallow
:type data: bool
"""
try:
serialized = cls.schema.dump(data, many=many)
return json.dumps(serialized, indent=cls.app.config.INDENT)
Expand Down
65 changes: 51 additions & 14 deletions src/biodm/components/controllers/resourcecontroller.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations
from functools import partial
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, List, Any

from starlette.routing import Mount, Route
from starlette.requests import Request
from starlette.responses import Response

from biodm.components.services import (
DatabaseService,
Expand Down Expand Up @@ -34,6 +36,9 @@ def overload_docstring(f):
Relevant SO posts:
- https://stackoverflow.com/questions/38125271/extending-or-overwriting-a-docstring-when-composing-classes
- https://stackoverflow.com/questions/1782843/python-decorator-handling-docstrings
:param f: The method we overload the docstrings of.
:type f: Callable
"""
async def wrapper(self, *args, **kwargs):
if self.app.config.DEV:
Expand All @@ -48,8 +53,19 @@ class ResourceController(EntityController):
"""Class for controllers exposing routes constituting a ressource.
Implements and exposes routes under a prefix named as the resource pluralized
that act as a standard REST-to-CRUD interface."""
that act as a standard REST-to-CRUD interface.
:param app: running server
:type app: Api
:param entity: entity name, defaults to None, inferred if None
:type entity: str, optional
:param table: entity table, defaults to None, inferred if None
:type table: Base, optional
:param schema: entity schema, defaults to None, inferred if None
:type schema: Schema, optional
"""
def __init__(self, app: Api, entity: str=None, table: Base=None, schema: Schema=None):
"""Constructor."""
super().__init__(app=app)
self.resource = entity if entity else self._infer_entity_name()
self.table = table if table else self._infer_table()
Expand Down Expand Up @@ -126,21 +142,35 @@ def routes(self, child_routes=None, **_) -> Mount:
Route(f'/{self.qp_id}', self.update, methods=[HttpMethod.PATCH.value]),
] + child_routes)

def _extract_pk_val(self, request):
def _extract_pk_val(self, request: Request) -> List[Any]:
"""Extracts id from request, raise exception if not found."""
pk_val = [request.path_params.get(k) for k in self.pk]
if not pk_val:
raise InvalidCollectionMethod
return pk_val

async def _extract_body(self, request):
async def _extract_body(self, request: Request) -> bytes:
"""Extracts body from request.
:param request: incomming request
:type request: Request
:raises PayloadEmptyError: in case payload is empty
:return: request body
:rtype: bytes
"""
body = await request.body()
if not body:
raise PayloadEmptyError
return body

async def create(self, request):
"""
async def create(self, request: Request) -> Response:
"""Creates associated entity.
:param request: incomming request
:type request: Request
:return: created object in JSON form
:rtype: Response
---
responses:
201:
description: Creates associated entity.
Expand All @@ -149,7 +179,7 @@ async def create(self, request):
204:
description: Empty Payload
"""
validated_data = self.deserialize(await self._extract_body(request))
validated_data = self.validate(await self._extract_body(request))
return json_response(
data=await self.svc.create(
data=validated_data,
Expand All @@ -161,8 +191,14 @@ async def create(self, request):
status_code=201,
)

async def read(self, request):
"""
async def read(self, request: Request) -> Response:
"""Fetch associated entity matching id in the path.
:param request: incomming request
:type request: Request
:return: JSON reprentation of the object
:rtype: Response
---
description: Query DB for entity with matching id.
parameters:
- in: path
Expand All @@ -179,6 +215,7 @@ async def read(self, request):
404:
description: Not Found
"""

fields = request.query_params.get('fields')
return json_response(
data=await self.svc.read(
Expand All @@ -189,11 +226,11 @@ async def read(self, request):
status_code=200,
)

async def update(self, request):
async def update(self, request: Request):
# TODO: Implement PATCH ?
raise NotImplementedError

async def delete(self, request):
async def delete(self, request: Request):
"""
description: Delete DB entry for entity with matching id.
parameters:
Expand All @@ -208,17 +245,17 @@ async def delete(self, request):
await self.svc.delete(pk_val=self._extract_pk_val(request))
return json_response("Deleted.", status_code=200)

async def create_update(self, request):
async def create_update(self, request: Request):
""""""
validated_data = self.deserialize(await self._extract_body(request))
validated_data = self.validate(await self._extract_body(request))
return json_response(
data=await self.svc.create_update(
pk_val=self._extract_pk_val(request), data=validated_data
),
status_code=200,
)

async def filter(self, request):
async def filter(self, request: Request):
"""
querystring shape:
prop1=val1: query for entries where prop1 = val1
Expand Down
7 changes: 4 additions & 3 deletions src/biodm/components/controllers/s3controller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path

from starlette.routing import Route, Mount
from starlette.requests import Request

from biodm.components.services import S3Service
from .controller import HttpMethod
Expand All @@ -24,18 +25,18 @@ def routes(self, child_routes=[], **_) -> Mount:
return super().routes(child_routes=child_routes + file_routes)

# TODO: Decorate with permissions.
async def download(self, request):
async def download(self, request: Request):
"""Returns aws s3 direct download URL with a redirect header.
"""
# TODO: Implement
pass

async def file_download_success(self, request):
async def file_download_success(self, request: Request):
"""Used as a callback in s3 presigned download urls for statistics."""
# TODO: Implement
pass

async def file_upload_success(self, request):
async def file_upload_success(self, request: Request):
""" Used as a callback in the s3 presigned upload urls that are emitted.
Uppon receival, update entity status in the DB."""

Expand Down
Loading

0 comments on commit c7956a4

Please sign in to comment.