feat(api): /v1 alias router for stable external contract (Issue A)
Mounts an explicit allowlist of public handlers under /v1 alongside the existing unversioned paths. External clients (AKC, OpenClaw, future tools) should target /v1; internal callers (hooks, wiki, admin UI) keep working unchanged. Breaking schema changes will bump the prefix to /v2. - src/atocore/main.py: _V1_PUBLIC_PATHS allowlist + second router - tests/test_v1_aliases.py: parity + OpenAPI presence (5 tests) - README.md: API versioning section - DEV-LEDGER.md: session log, test_count 459 -> 463 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from atocore import __version__
|
||||
from atocore.api.routes import router
|
||||
@@ -53,6 +54,57 @@ app = FastAPI(
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
# Public API v1 — stable contract for external clients (AKC, OpenClaw, etc.).
|
||||
# Paths listed here are re-mounted under /v1 as aliases of the existing
|
||||
# unversioned handlers. Unversioned paths continue to work; new endpoints
|
||||
# land at the latest version; breaking schema changes bump the prefix.
|
||||
_V1_PUBLIC_PATHS = {
|
||||
"/entities",
|
||||
"/entities/{entity_id}",
|
||||
"/entities/{entity_id}/promote",
|
||||
"/entities/{entity_id}/reject",
|
||||
"/entities/{entity_id}/audit",
|
||||
"/relationships",
|
||||
"/ingest",
|
||||
"/ingest/sources",
|
||||
"/context/build",
|
||||
"/query",
|
||||
"/projects",
|
||||
"/projects/{project_name}",
|
||||
"/projects/{project_name}/refresh",
|
||||
"/projects/{project_name}/mirror",
|
||||
"/projects/{project_name}/mirror.html",
|
||||
"/memory",
|
||||
"/memory/{memory_id}",
|
||||
"/memory/{memory_id}/audit",
|
||||
"/memory/{memory_id}/promote",
|
||||
"/memory/{memory_id}/reject",
|
||||
"/project/state",
|
||||
"/project/state/{project_name}",
|
||||
"/interactions",
|
||||
"/interactions/{interaction_id}",
|
||||
"/interactions/{interaction_id}/reinforce",
|
||||
"/interactions/{interaction_id}/extract",
|
||||
"/health",
|
||||
"/sources",
|
||||
"/stats",
|
||||
}
|
||||
|
||||
_v1_router = APIRouter(prefix="/v1", tags=["v1"])
|
||||
for _route in list(router.routes):
|
||||
if isinstance(_route, APIRoute) and _route.path in _V1_PUBLIC_PATHS:
|
||||
_v1_router.add_api_route(
|
||||
_route.path,
|
||||
_route.endpoint,
|
||||
methods=list(_route.methods),
|
||||
response_model=_route.response_model,
|
||||
response_class=_route.response_class,
|
||||
name=f"v1_{_route.name}",
|
||||
include_in_schema=True,
|
||||
)
|
||||
app.include_router(_v1_router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
Reference in New Issue
Block a user