feat(api): Initial API server and stamps routes.
This commit is contained in:
231
src/routes/documents.py
Normal file
231
src/routes/documents.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
- `/documents` with `POST, GET`
|
||||
- `/documents/{document_id}` with `GET`, `PATCH`, `DELETE`
|
||||
- `/documents/{document_id}/stamps` which is passed to `/stamps` with `document_id` set.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Any, NamedTuple, Optional, Self, TypedDict, Unpack, reveal_type, List
|
||||
from aiohttp import web
|
||||
from data import Condition, condition
|
||||
from data.queries import JOINTYPE
|
||||
from datamodels import DataModel
|
||||
|
||||
from .lib import ModelField, datamodelsv
|
||||
from .stamps import Stamp, StampCreateParams, StampEditParams, StampPayload
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
|
||||
|
||||
class DocPayload(TypedDict):
|
||||
document_id: int
|
||||
document_data: str
|
||||
seal: int
|
||||
created_at: str
|
||||
metadata: Optional[str]
|
||||
stamps: List[StampPayload]
|
||||
|
||||
|
||||
class DocCreateParamsReq(TypedDict, total=True):
|
||||
document_data: str
|
||||
seal: int
|
||||
|
||||
|
||||
class DocCreateParams(DocCreateParamsReq, total=False):
|
||||
metadata: Optional[str]
|
||||
stamps: List[StampCreateParams]
|
||||
|
||||
|
||||
class DocEditParams(TypedDict, total=False):
|
||||
document_data: str
|
||||
seal: int
|
||||
metadata: Optional[str]
|
||||
stamps: List[StampCreateParams]
|
||||
|
||||
|
||||
fields = [
|
||||
ModelField('document_id', int, False, False, False),
|
||||
ModelField('document_data', str, True, True, True),
|
||||
ModelField('seal', int, True, True, True),
|
||||
ModelField('created_at', str, False, False, False),
|
||||
ModelField('metadata', Optional[str], False, True, True),
|
||||
ModelField('stamps', List[StampCreateParams], False, True, True),
|
||||
]
|
||||
req_fields = {field.name for field in fields if field.required}
|
||||
edit_fields = {field.name for field in fields if field.can_edit}
|
||||
create_fields = {field.name for field in fields if field.can_create}
|
||||
|
||||
|
||||
class Document:
|
||||
def __init__(self, app: web.Application, row: DataModel.Document):
|
||||
self.app = app
|
||||
self.data = app[datamodelsv]
|
||||
self.row = row
|
||||
|
||||
@classmethod
|
||||
async def fetch_from_id(cls, app: web.Application, document_id: int) -> Optional[Self]:
|
||||
data = app[datamodelsv]
|
||||
row = await data.Document.fetch(document_id)
|
||||
return cls(app, row) if row is not None else None
|
||||
|
||||
@classmethod
|
||||
async def query(
|
||||
cls,
|
||||
app: web.Application,
|
||||
document_id: Optional[int] = None,
|
||||
seal: Optional[int] = None,
|
||||
created_before: Optional[str] = None,
|
||||
created_after: Optional[str] = None,
|
||||
metadata: Optional[str] = None,
|
||||
stamp_type: Optional[str] = None,
|
||||
) -> List[Self]:
|
||||
data = app[datamodelsv]
|
||||
Doc = data.Document
|
||||
|
||||
conds = []
|
||||
if document_id is not None:
|
||||
conds.append(Doc.document_id == document_id)
|
||||
if seal is not None:
|
||||
conds.append(Doc.seal == seal)
|
||||
if created_before is not None:
|
||||
cbefore = datetime.fromisoformat(created_before)
|
||||
conds.append(Doc.created_at <= cbefore)
|
||||
if created_after is not None:
|
||||
cafter = datetime.fromisoformat(created_after)
|
||||
conds.append(Doc.created_at >= cafter)
|
||||
if metadata is not None:
|
||||
conds.append(Doc.metadata == metadata)
|
||||
|
||||
query = data.Document.table.fetch_rows_where(*conds)
|
||||
results = await query
|
||||
|
||||
query = data.Document.table.select_where(*conds)
|
||||
if stamp_type is not None:
|
||||
query.join('document_stamps', using=('document_id',), join_type=JOINTYPE.LEFT)
|
||||
query.join(
|
||||
'stamp_types',
|
||||
on=(data.DocumentStamp.stamp_type == data.StampType.stamp_type_id)
|
||||
)
|
||||
query.where(data.StampType.stamp_type_name == stamp_type)
|
||||
query.select(docid = "DISTINCT(document_id)")
|
||||
query.with_no_adapter()
|
||||
results = await query
|
||||
ids = [result['docid'] for result in results]
|
||||
if ids:
|
||||
rows = await data.Document.table.fetch_rows_where(
|
||||
document_id=ids
|
||||
)
|
||||
else:
|
||||
rows = []
|
||||
else:
|
||||
rows = await query
|
||||
|
||||
return [cls(app, row) for row in rows]
|
||||
|
||||
@classmethod
|
||||
async def create(cls, app: web.Application, **kwargs: Unpack[DocCreateParams]) -> Self:
|
||||
data = app[datamodelsv]
|
||||
|
||||
document_data = kwargs['document_data']
|
||||
seal = kwargs['seal']
|
||||
stamp_params = kwargs.get('stamps', [])
|
||||
metadata = kwargs.get('metadata')
|
||||
|
||||
# Build the document first
|
||||
row = await data.Document.create(
|
||||
document_data=document_data,
|
||||
seal=seal,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
# Then build the stamps
|
||||
stamps = []
|
||||
for stampdata in stamp_params:
|
||||
stampdata.setdefault('document_id', row.document_id)
|
||||
stamp = await Stamp.create(app, **stampdata)
|
||||
stamps.append(stamp)
|
||||
|
||||
return cls(app, row)
|
||||
|
||||
async def get_stamps(self) -> List[Stamp]:
|
||||
stamprows = await self.data.DocumentStamp.table.fetch_rows_where(document_id=self.row.document_id)
|
||||
return [Stamp(self.app, row) for row in stamprows]
|
||||
|
||||
async def prepare(self) -> DocPayload:
|
||||
stamps = await self.get_stamps()
|
||||
|
||||
results: DocPayload = {
|
||||
'document_id': self.row.document_id,
|
||||
'document_data': self.row.document_data,
|
||||
'seal': self.row.seal,
|
||||
'created_at': self.row.created_at.isoformat(),
|
||||
'metadata': self.row.metadata,
|
||||
'stamps': [await stamp.prepare() for stamp in stamps]
|
||||
}
|
||||
return results
|
||||
|
||||
async def edit(self, **kwargs: Unpack[DocEditParams]):
|
||||
data = self.data
|
||||
row = self.row
|
||||
# Update the row data
|
||||
# If stamps are given, delete the existing ones
|
||||
# Then write in the new ones.
|
||||
update_args = {}
|
||||
for key in {'document_data', 'seal', 'metadata'}:
|
||||
if key in kwargs:
|
||||
update_args[key] = kwargs[key]
|
||||
if update_args:
|
||||
await self.row.update(**update_args)
|
||||
|
||||
# TODO: Should really be in a transaction
|
||||
# Actually each handler should be in a transaction
|
||||
if new_stamps := kwargs.get('stamps', []):
|
||||
await self.data.DocumentStamp.table.delete_where(document_id=self.row.document_id)
|
||||
for stampdata in new_stamps:
|
||||
stampdata.setdefault('document_id', row.document_id)
|
||||
await Stamp.create(self.app, **stampdata)
|
||||
|
||||
async def delete(self) -> DocPayload:
|
||||
payload = await self.prepare()
|
||||
await self.row.delete()
|
||||
return payload
|
||||
|
||||
|
||||
@routes.view('/documents')
|
||||
class DocumentsView(web.View):
|
||||
async def get(self):
|
||||
request = self.request
|
||||
filter_params = {}
|
||||
keys = [
|
||||
'document_id',
|
||||
'seal',
|
||||
'created_before',
|
||||
'created_after',
|
||||
'metadata',
|
||||
'stamp_type',
|
||||
]
|
||||
for key in keys:
|
||||
if key in request.query:
|
||||
filter_params[key] = request.query[key]
|
||||
elif key in request:
|
||||
filter_params[key] = request[key]
|
||||
|
||||
documents = await Document.query(request.app, **filter_params)
|
||||
payload = [await doc.prepare() for doc in documents]
|
||||
return web.json_response(payload)
|
||||
|
||||
async def post(self):
|
||||
request = self.request
|
||||
|
||||
params = await request.json()
|
||||
for key in create_fields:
|
||||
if key in request:
|
||||
params.setdefault(key, request[key])
|
||||
|
||||
if extra := next((key for key in params if key not in create_fields), None):
|
||||
raise web.HTTPBadRequest(text=f"Invalid key '{extra}' passed to document creation.")
|
||||
if missing := next((key for key in req_fields if key not in params), None):
|
||||
raise web.HTTPBadRequest(text=f"Document params missing required key '{missing}'")
|
||||
|
||||
document = await Document.create(self.request.app, **params)
|
||||
payload = await document.prepare()
|
||||
return web.json_response(payload)
|
||||
Reference in New Issue
Block a user