Skip to content

Commit ba5f644

Browse files
jopemachineclaude
andcommitted
feat(BA-5830): add AppConfigFragment REST v2 endpoints
Mounts the Fragment adapter as REST v2 under `/v2/app-config-fragments` (BEP-1052 §4). Shares v2 DTOs with the GraphQL surface added in BA-5829 (#11285). Endpoints: - `POST /v2/app-config-fragments/get` — auth_required (body-carried natural key) - `POST /v2/app-config-fragments/{scope_type}/{scope_id}/search` — auth_required (scoped) - `POST /v2/app-config-fragments/search` — superadmin_required (cross-scope) - `POST /v2/app-config-fragments` — superadmin_required (create) - `POST /v2/app-config-fragments/update` — superadmin_required - `POST /v2/app-config-fragments/purge` — superadmin_required Scoped search uses the project's `{scope_type}/{scope_id}` nested URL convention; `get` takes the three-part natural key via the body since a three-field path segment is noisier than a body DTO. Adds: - `api/rest/v2/app_config_fragment/handler.V2AppConfigFragmentHandler` + `registry.register_v2_app_config_fragment_routes`. - `AppConfigFragmentScopePathParam` in the shared path-params module. - Handler instantiation + sub-registry wiring in `rest/v2/tree.py`. The scope handler converts the DTO `AppConfigScopeType` to the data-layer enum so the adapter signature stays data-typed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 56336f7 commit ba5f644

5 files changed

Lines changed: 151 additions & 0 deletions

File tree

src/ai/backend/manager/api/rest/v2/app_config_fragment/__init__.py

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""REST v2 handler for the app-config fragment domain (BEP-1052 §2)."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from http import HTTPStatus
7+
from typing import TYPE_CHECKING, Final
8+
9+
from ai.backend.common.api_handlers import APIResponse, BodyParam, PathParam
10+
from ai.backend.common.dto.manager.v2.app_config_fragment.request import (
11+
AppConfigFragmentKeyInput,
12+
CreateAppConfigFragmentInput,
13+
PurgeAppConfigFragmentInput,
14+
SearchAppConfigFragmentsInput,
15+
UpdateAppConfigFragmentInput,
16+
)
17+
from ai.backend.common.dto.manager.v2.app_config_fragment.types import AppConfigScopeType
18+
from ai.backend.logging import BraceStyleAdapter
19+
from ai.backend.manager.api.rest.v2.path_params import AppConfigFragmentScopePathParam
20+
from ai.backend.manager.data.app_config_fragment.types import (
21+
AppConfigScopeType as DataAppConfigScopeType,
22+
)
23+
24+
if TYPE_CHECKING:
25+
from ai.backend.manager.api.adapters.app_config_fragment import AppConfigFragmentAdapter
26+
27+
log: Final = BraceStyleAdapter(logging.getLogger(__spec__.name))
28+
29+
30+
class V2AppConfigFragmentHandler:
31+
"""REST v2 handler for app-config fragment operations."""
32+
33+
def __init__(self, *, adapter: AppConfigFragmentAdapter) -> None:
34+
self._adapter = adapter
35+
36+
async def get(
37+
self,
38+
body: BodyParam[AppConfigFragmentKeyInput],
39+
) -> APIResponse:
40+
"""Read a single fragment by natural key (any authenticated user)."""
41+
result = await self._adapter.get(body.parsed)
42+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
43+
44+
async def scoped_search(
45+
self,
46+
path: PathParam[AppConfigFragmentScopePathParam],
47+
body: BodyParam[SearchAppConfigFragmentsInput],
48+
) -> APIResponse:
49+
"""Scope-bound fragment search — caller is pinned to a specific
50+
`(scope_type, scope_id)` pair via the URL path.
51+
"""
52+
result = await self._adapter.search(
53+
scope_type=DataAppConfigScopeType(path.parsed.scope_type),
54+
scope_id=path.parsed.scope_id,
55+
input=body.parsed,
56+
)
57+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
58+
59+
async def admin_search(
60+
self,
61+
body: BodyParam[SearchAppConfigFragmentsInput],
62+
) -> APIResponse:
63+
"""Cross-scope admin search (admin only)."""
64+
result = await self._adapter.admin_search(body.parsed)
65+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
66+
67+
async def admin_create(
68+
self,
69+
body: BodyParam[CreateAppConfigFragmentInput],
70+
) -> APIResponse:
71+
"""Create a new fragment (admin only)."""
72+
result = await self._adapter.create(body.parsed)
73+
return APIResponse.build(status_code=HTTPStatus.CREATED, response_model=result)
74+
75+
async def admin_update(
76+
self,
77+
body: BodyParam[UpdateAppConfigFragmentInput],
78+
) -> APIResponse:
79+
"""Update a fragment's `extra_config` (admin only)."""
80+
result = await self._adapter.update(body.parsed)
81+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
82+
83+
async def admin_purge(
84+
self,
85+
body: BodyParam[PurgeAppConfigFragmentInput],
86+
) -> APIResponse:
87+
"""Hard-delete a fragment (admin only)."""
88+
result = await self._adapter.purge(body.parsed)
89+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
90+
91+
92+
# ``AppConfigScopeType`` is imported for OpenAPI schema visibility of the
93+
# string-form path parameter; keep the import alive.
94+
_ = AppConfigScopeType
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Route registration for v2 app-config fragment endpoints."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from ai.backend.manager.api.rest.middleware.auth import auth_required, superadmin_required
8+
from ai.backend.manager.api.rest.routing import RouteRegistry
9+
10+
from .handler import V2AppConfigFragmentHandler
11+
12+
if TYPE_CHECKING:
13+
from ai.backend.manager.api.rest.types import RouteDeps
14+
15+
16+
def register_v2_app_config_fragment_routes(
17+
handler: V2AppConfigFragmentHandler,
18+
route_deps: RouteDeps,
19+
) -> RouteRegistry:
20+
"""Register all v2 app-config fragment routes (BEP-1052 §4).
21+
22+
- `POST /v2/app-config-fragments/get` reads a single row via body
23+
(the natural key has three fields, so a body DTO is cleaner than
24+
a long path).
25+
- Scoped search mounts at
26+
`/v2/app-config-fragments/{scope_type}/{scope_id}/search` per the
27+
project's scoped-search URL convention.
28+
- Admin cross-scope search, create / update / purge are admin-only.
29+
"""
30+
reg = RouteRegistry.create("app-config-fragments", route_deps.cors_options)
31+
32+
# Read
33+
reg.add("POST", "/get", handler.get, middlewares=[auth_required])
34+
reg.add(
35+
"POST",
36+
"/{scope_type}/{scope_id}/search",
37+
handler.scoped_search,
38+
middlewares=[auth_required],
39+
)
40+
reg.add("POST", "/search", handler.admin_search, middlewares=[superadmin_required])
41+
# Write
42+
reg.add("POST", "", handler.admin_create, middlewares=[superadmin_required])
43+
reg.add("POST", "/update", handler.admin_update, middlewares=[superadmin_required])
44+
reg.add("POST", "/purge", handler.admin_purge, middlewares=[superadmin_required])
45+
46+
return reg

src/ai/backend/manager/api/rest/v2/path_params.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
from ai.backend.common.api_handlers import BaseRequestModel
1010

1111

12+
class AppConfigFragmentScopePathParam(BaseRequestModel):
13+
scope_type: str = Field(description="App-config scope type (public/domain/user/...).")
14+
scope_id: str = Field(description="Scope id (domain name, user id, or `public`).")
15+
16+
1217
class DomainNamePathParam(BaseRequestModel):
1318
domain_name: str = Field(description="Domain name")
1419

src/ai/backend/manager/api/rest/v2/tree.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def build_v2_routes(
2828
# Lazy imports to avoid circular dependencies at module level
2929
from .agent.handler import V2AgentHandler
3030
from .agent.registry import register_v2_agent_routes
31+
from .app_config_fragment.handler import V2AppConfigFragmentHandler
32+
from .app_config_fragment.registry import register_v2_app_config_fragment_routes
3133
from .artifact.handler import V2ArtifactHandler
3234
from .artifact.registry import register_v2_artifact_routes
3335
from .artifact_registry.handler import V2ArtifactRegistryHandler
@@ -111,6 +113,7 @@ def build_v2_routes(
111113

112114
# Build all handlers (each takes its individual adapter)
113115
agent_handler = V2AgentHandler(adapter=adapters.agent)
116+
app_config_fragment_handler = V2AppConfigFragmentHandler(adapter=adapters.app_config_fragment)
114117
artifact_handler = V2ArtifactHandler(adapter=adapters.artifact)
115118
artifact_registry_handler = V2ArtifactRegistryHandler(adapter=adapters.artifact_registry)
116119
audit_log_handler = V2AuditLogHandler(adapter=adapters.audit_log)
@@ -165,6 +168,9 @@ def build_v2_routes(
165168

166169
# Add all domain sub-registries
167170
v2_reg.add_subregistry(register_v2_agent_routes(agent_handler, route_deps))
171+
v2_reg.add_subregistry(
172+
register_v2_app_config_fragment_routes(app_config_fragment_handler, route_deps)
173+
)
168174
v2_reg.add_subregistry(register_v2_artifact_routes(artifact_handler, route_deps))
169175
v2_reg.add_subregistry(
170176
register_v2_artifact_registry_routes(artifact_registry_handler, route_deps)

0 commit comments

Comments
 (0)