From ff3c4a00e55ace7ebca6b9408680e69164f16259 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 28 Jan 2026 19:04:18 +0000 Subject: [PATCH] Modules missing git add --- paskia/fastapi/response.py | 22 ++++++++ paskia/util/apistructs.py | 110 +++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 paskia/fastapi/response.py create mode 100644 paskia/util/apistructs.py diff --git a/paskia/fastapi/response.py b/paskia/fastapi/response.py new file mode 100644 index 0000000..6e96180 --- /dev/null +++ b/paskia/fastapi/response.py @@ -0,0 +1,22 @@ +"""FastAPI response utilities for msgspec.Struct serialization.""" + +import msgspec +from fastapi import Response + + +class MsgspecResponse(Response): + """Response that uses msgspec for JSON encoding. + + Use this for returning msgspec.Struct, dict, or list with proper serialization. + """ + + media_type = "application/json" + + def __init__( + self, + content: msgspec.Struct | dict | list, + status_code: int = 200, + headers: dict | None = None, + ): + body = msgspec.json.encode(content) + super().__init__(content=body, status_code=status_code, headers=headers) diff --git a/paskia/util/apistructs.py b/paskia/util/apistructs.py new file mode 100644 index 0000000..5cc31d6 --- /dev/null +++ b/paskia/util/apistructs.py @@ -0,0 +1,110 @@ +"""API response utilities using msgspec for JSON serialization. + +msgspec handles UUID and datetime conversion automatically. +API structs inherit from db structs with kw_only=True to add uuid/key fields. +""" + +from datetime import UTC, datetime +from uuid import UUID + +import msgspec + +from paskia.db.structs import Org, Permission, Role, User +from paskia.util import useragent + + +def _utc_datetime(dt: datetime | None) -> datetime | None: + """Convert datetime to UTC, handling both aware and naive datetimes.""" + if dt is None: + return None + if dt.tzinfo: + return dt.astimezone(UTC) + return dt.replace(tzinfo=UTC) + + +def format_datetime(dt: datetime | None) -> str | None: + """Format a datetime to ISO 8601 string with Z suffix for UTC.""" + if dt is None: + return None + utc_dt = _utc_datetime(dt) + return utc_dt.isoformat().replace("+00:00", "Z") if utc_dt else None + + +# ------------------------------------------------------------------------- +# API structs - inherit from db structs, add uuid for serialization +# ------------------------------------------------------------------------- + + +class ApiUser(User, kw_only=True): + """User with uuid serialized.""" + + uuid: UUID + + @classmethod + def from_db(cls, u: User) -> "ApiUser": + return cls(uuid=u.uuid, **msgspec.structs.asdict(u)) + + +class ApiOrg(Org, kw_only=True): + """Org with uuid serialized.""" + + uuid: UUID + + @classmethod + def from_db(cls, o: Org) -> "ApiOrg": + return cls(uuid=o.uuid, **msgspec.structs.asdict(o)) + + +class ApiRole(Role, kw_only=True): + """Role with uuid serialized.""" + + uuid: UUID + + @classmethod + def from_db(cls, r: Role) -> "ApiRole": + return cls(uuid=r.uuid, **msgspec.structs.asdict(r)) + + +class ApiPermission(Permission, kw_only=True): + """Permission with uuid serialized.""" + + uuid: UUID + + @classmethod + def from_db(cls, p: Permission) -> "ApiPermission": + return cls(uuid=p.uuid, **msgspec.structs.asdict(p)) + + +class ApiSession(msgspec.Struct): + """Session for API responses with computed fields.""" + + id: str + credential_uuid: UUID = msgspec.field(name="credential") + host: str + ip: str + user_agent: str + last_renewed: datetime + is_current: bool = False + is_current_host: bool = False + + @classmethod + def from_db( + cls, + s, # Session + *, + current_key: str, + normalized_host: str | None, + expires_delta, # timedelta + ) -> "ApiSession": + return cls( + id=s.key, + credential_uuid=s.credential_uuid, + host=s.host, + ip=s.ip, + user_agent=useragent.compact_user_agent(s.user_agent), + last_renewed=s.expiry - expires_delta, + is_current=s.key == current_key, + is_current_host=bool( + normalized_host and s.host and s.host == normalized_host + ), + )