Skip to content

Commit

Permalink
feature(api): Log routes generally work - Quarto watcher is now async.
Browse files Browse the repository at this point in the history
``BaseRouter`` now defines a method for building a router from class
metadata (which is not exactly the class router).
``get_routes`` is now part of ``BaseRouter`` and includes ``websockets`` and
``prefix`` data. Added corresponding schemas.
Factored out ``log`` collection router code that would be the same.
``/api/dev/log`` and ``/api/dev/quarto`` Websockets roughly work.
All api routes will have the ``api`` prefix.
Factored out logging collection queries and put with ``schemas``.
``BlogHandler`` is now async and is run by ``watchfiles``
No longer using ``watchdog``.
  • Loading branch information
acederberg committed Dec 3, 2024
1 parent 3e8125f commit 99732c9
Show file tree
Hide file tree
Showing 9 changed files with 571 additions and 409 deletions.
69 changes: 44 additions & 25 deletions acederbergio/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import fastapi

from acederbergio import env
from acederbergio.api import schemas

logger = env.create_logger(__name__)

AnyRouter = fastapi.APIRouter | fastapi.FastAPI


class RouterMixins:
"""
Expand All @@ -18,10 +21,19 @@ class RouterMixins:

# router_children: ClassVar[dict[str, Type]] = dict()
router_children: ClassVar[dict[str, "RouterMeta"]] = dict()
router: ClassVar[fastapi.APIRouter | fastapi.FastAPI]
router: ClassVar[AnyRouter]
router_args: ClassVar[dict[str, Any]] = dict() # type: ignore
router_routes: ClassVar[dict[str, str | dict[str, Any]]] = dict() # type: ignore

@classmethod
async def get_routes(cls, request: fastapi.Request) -> schemas.AppInfo:
prefix = request.url.path.replace("/routes", "")
return schemas.AppInfo.fromRouter(cls.router, prefix=prefix)

@staticmethod
def get_it_works():
return "It works!"


class RouterMeta(type):
"""Metaclass to handle routing.
Expand All @@ -30,7 +42,9 @@ class RouterMeta(type):
"""

@classmethod
def add_route(cls, T, fn_name: str, fn_info_raw: str | dict[str, Any]):
def add_route(
cls, T, router: AnyRouter, *, fn_name: str, fn_info_raw: str | dict[str, Any]
):
name = T.__name__

# NOTE: Annotation is stange bc of the following mypy error:
Expand All @@ -49,14 +63,19 @@ def add_route(cls, T, fn_name: str, fn_info_raw: str | dict[str, Any]):

# Parse name
raw, _ = fn_name.split("_", 1)
http_meth = next((hh for hh in HTTPMethod if hh.value.lower() == raw), None)
if http_meth is None:
logger.warning(f"Could not determine method of `{fn_name}`.")
return
if raw == "websocket":
meth = "websocket"
else:
meth = next((hh for hh in HTTPMethod if hh.value.lower() == raw), None)
if meth is None:
logger.warning(f"Could not determine method of `{fn_name}`.")
return

# Update status code if not provided.
if http_meth == HTTPMethod.POST and "status" not in info:
info.update(status_code=201)
# Update status code if not provided.
if meth == HTTPMethod.POST and "status" not in info:
info.update(status_code=201)

meth = meth.value.lower()

# Find attr
fn = getattr(T, fn_name, None)
Expand All @@ -66,7 +85,7 @@ def add_route(cls, T, fn_name: str, fn_info_raw: str | dict[str, Any]):

# Get the decoerator and call it.
logger.debug("Adding function `%s` at url `%s`.", fn.__name__, url)
decorator = getattr(T.router, http_meth.value.lower())
decorator = getattr(router, meth.lower())
decorator(url, **info)(fn)

def __new__(cls, name, bases, namespace):
Expand All @@ -92,28 +111,28 @@ def __new__(cls, name, bases, namespace):
raise ValueError(f"`{name}.router_args` must be a `dict`.")

if name != "BaseView":
# Create router.
logger.debug("Creating router for `%s`.", name)
T.router = ( # type: ignore
router = ( # type: ignore
T.router # type: ignore
if hasattr(T, "router")
else fastapi.APIRouter(**T.router_args) # type: ignore
)
for fn_name, fn_info in T.router_routes.items(): # type: ignore
cls.add_route(T, fn_name, fn_info)

for child_prefix, child in T.router_children.items(): # type: ignore
logger.debug(
"Adding child router `%s` for `%s`.",
child_prefix,
name,
)
T.router.include_router( # type: ignore
child.router,
prefix=child_prefix,
)
cls.create_router(T, router)
T.router = router

return T

@classmethod
def create_router(cls, T, router: AnyRouter):
# Create router.
for fn_name, fn_info in T.router_routes.items(): # type: ignore
cls.add_route(T, router, fn_name=fn_name, fn_info_raw=fn_info)

for child_prefix, child in T.router_children.items(): # type: ignore
router.include_router( # type: ignore
child.router,
prefix=child_prefix,
)


class Router(RouterMixins, metaclass=RouterMeta): ...
8 changes: 4 additions & 4 deletions acederbergio/api/depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ def db_config() -> db.Config:
return db.Config() # type: ignore


def db_client(config: "DbConfig") -> motor.motor_asyncio.AsyncIOMotorDatabase:
def db_db(config: "DbConfig") -> motor.motor_asyncio.AsyncIOMotorDatabase:
return config.create_client_async()[config.database]


DbConfig = Annotated[db.Config, fastapi.Depends(db_config, use_cache=True)]
DbClient = Annotated[
motor.motor_asyncio.AsyncIOMotorClient,
fastapi.Depends(db_client, use_cache=False),
Db = Annotated[
motor.motor_asyncio.AsyncIOMotorDatabase,
fastapi.Depends(db_db, use_cache=False),
]
Loading

0 comments on commit 99732c9

Please sign in to comment.