diff --git a/.claude/docs/architectural_patterns.md b/.claude/docs/architectural_patterns.md index 4a5c9c494..4bf214b2e 100644 --- a/.claude/docs/architectural_patterns.md +++ b/.claude/docs/architectural_patterns.md @@ -85,7 +85,7 @@ Request → Middleware → APIRouter → Endpoint function → Service layer → ### Endpoint modules: single file or package -A module under `sparkth/api/v1/` takes one of two shapes. `sparkth/api/v1/api.py` mounts +A module under `sparkth/api/v1/` takes one of two shapes. `sparkth/api/v1/__init__.py` mounts each one the same way — `include_router(.router, prefix=...)` — so the shape is an internal organisation choice and changing it never moves a URL. @@ -103,9 +103,9 @@ sparkth/api/v1// schemas.py # request/response models owned by this module ``` -- `__init__.py` re-exports `router` and carries `__all__ = ["router"]`. This is a - deliberate exception to the "avoid re-exports" rule: it keeps `api.py`'s mounting - uniform across both shapes. +- The endpoint package's own `__init__.py` re-exports `router` and carries + `__all__ = ["router"]`. This is a deliberate exception to the "avoid re-exports" rule: + it keeps the mounting in `sparkth/api/v1/__init__.py` uniform across both shapes. - It is also where `register_exception_handler(ExcClass, status_code)` calls live, so a domain's exception → status mapping sits beside the routes that raise it (see section 8, and `permissions/__init__.py` / `whitelist/__init__.py`). A package with no domain @@ -118,9 +118,9 @@ sparkth/api/v1// into a package makes every other domain import that package, and importing any module from it executes its `__init__.py` and therefore its `routes.py`, which turns a later import in the other direction into a circular one. -- Route paths come from the prefix in `api.py`, never from the package name, so - converting a file to a package is a pure refactor: the OpenAPI document, the generated - frontend client, and every URL stay byte-identical. +- Route paths come from the prefix in `sparkth/api/v1/__init__.py`, never from the package + name, so converting a file to a package is a pure refactor: the OpenAPI document, the + generated frontend client, and every URL stay byte-identical. --- diff --git a/sparkth/api/v1/__init__.py b/sparkth/api/v1/__init__.py index e69de29bb..e570a92c6 100644 --- a/sparkth/api/v1/__init__.py +++ b/sparkth/api/v1/__init__.py @@ -0,0 +1,28 @@ +"""Version 1 of the REST API. + +Assembles ``api_router`` from every endpoint module in this package. Each module +exposes a ``router`` — whether it is a single file (``auth.py``) or a package with +its own ``routes.py`` and ``schemas.py`` (``user/``) — and is mounted here under +its URL prefix and OpenAPI tag. ``sparkth.main.assemble_app`` mounts the result at +``/api/v1``, so every path in this file is relative to that. + +Prefixes are the public URL contract: changing one here moves an endpoint for every +client, independently of how the module behind it is laid out on disk. +""" + +from fastapi import APIRouter + +from sparkth.api.v1 import analytics, auth, file_parser, language, llm, permissions, user, user_plugins, whitelist + +api_router = APIRouter() +api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) +api_router.include_router(user.router, prefix="/user", tags=["user"]) +api_router.include_router(language.router, prefix="/languages", tags=["Languages"]) +api_router.include_router(user_plugins.router, prefix="/user-plugins", tags=["User Plugins"]) +api_router.include_router(file_parser.router, prefix="/parser", tags=["File Parser"]) +api_router.include_router(whitelist.router, prefix="/whitelist", tags=["Whitelist"]) +api_router.include_router(llm.router, prefix="/llm", tags=["LLM Configuration"]) +api_router.include_router(permissions.router, prefix="/permissions", tags=["Permissions"]) +api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytics"]) + +__all__ = ["api_router"] diff --git a/sparkth/api/v1/api.py b/sparkth/api/v1/api.py deleted file mode 100644 index 2f701e894..000000000 --- a/sparkth/api/v1/api.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import APIRouter - -from sparkth.api.v1 import analytics, auth, file_parser, language, llm, permissions, user, user_plugins, whitelist - -api_router = APIRouter() -api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(user.router, prefix="/user", tags=["user"]) -api_router.include_router(language.router, prefix="/languages", tags=["Languages"]) -api_router.include_router(user_plugins.router, prefix="/user-plugins", tags=["User Plugins"]) -api_router.include_router(file_parser.router, prefix="/parser", tags=["File Parser"]) -api_router.include_router(whitelist.router, prefix="/whitelist", tags=["Whitelist"]) -api_router.include_router(llm.router, prefix="/llm", tags=["LLM Configuration"]) -api_router.include_router(permissions.router, prefix="/permissions", tags=["Permissions"]) -api_router.include_router(analytics.router, prefix="/analytics", tags=["Analytics"]) diff --git a/sparkth/main.py b/sparkth/main.py index 01b98cf78..f8db47cf7 100644 --- a/sparkth/main.py +++ b/sparkth/main.py @@ -8,7 +8,7 @@ from starlette.types import Lifespan import sparkth.lib.audit.callbacks # noqa: F401 # registers the process-global LangChain audit callback on import -from sparkth.api.v1.api import api_router +from sparkth.api.v1 import api_router from sparkth.core.audit.middleware import AuditContextMiddleware from sparkth.core.config import MCP_MOUNT_PATH, get_settings from sparkth.core.exceptions.handlers import EXCEPTION_HANDLERS