Skip to content

Commit 13cf1ab

Browse files
jopemachineclaude
andcommitted
feat(BA-5827): add AppConfigFragment bulk service surface (BEP-1052 §3)
Adds bulk service operations alongside the single-item ones: - New action files: admin_bulk_{create,update,purge} and bulk_{create,update}_my (self-service variants need user_id injected by the adapter via current_user()) - data/app_config_fragment/bulk_types.py for shared service-layer dataclasses (AppConfigFragmentBulkItem, MyAppConfigFragmentBulkItem, AppConfigFragmentBulkItemError) - Service methods admin_bulk_* and bulk_*_my with per-item try/except for partial-success semantics; my_bulk_* recompute the merged AppConfig view per success (BEP-1052 §3) - Processor wiring + supported_actions registration - Adapter exposes admin_bulk_{create,update,purge}; my_bulk adapter methods land with the merged-view DTO surface in BA-5829 because their payloads carry AppConfigNode - DTO additions: bulk inputs and admin_bulk payloads. my_bulk payloads stay with the AppConfig DTOs because they reference AppConfigNode (added with merged-view DTO) Single-item create/update/purge service methods stay so they back the read paths and any internal callers; the public API surface (added in BA-5829 / BA-5830) is bulk-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f575b11 commit 13cf1ab

12 files changed

Lines changed: 701 additions & 0 deletions

File tree

src/ai/backend/common/dto/manager/v2/app_config_fragment/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
from .request import (
2+
AdminAppConfigFragmentItemInput,
3+
AdminBulkCreateAppConfigFragmentsInput,
4+
AdminBulkPurgeAppConfigFragmentsInput,
5+
AdminBulkUpdateAppConfigFragmentsInput,
26
AppConfigFragmentFilter,
37
AppConfigFragmentKeyInput,
48
AppConfigFragmentOrder,
9+
BulkCreateMyAppConfigFragmentsInput,
10+
BulkUpdateMyAppConfigFragmentsInput,
511
CreateAppConfigFragmentInput,
12+
MyAppConfigFragmentItemInput,
613
PurgeAppConfigFragmentInput,
714
SearchAppConfigFragmentsInput,
815
UpdateAppConfigFragmentInput,
916
)
1017
from .response import (
18+
AdminBulkCreateAppConfigFragmentsPayload,
19+
AdminBulkPurgeAppConfigFragmentsPayload,
20+
AdminBulkUpdateAppConfigFragmentsPayload,
21+
AppConfigFragmentBulkError,
1122
AppConfigFragmentNode,
1223
CreateAppConfigFragmentPayload,
1324
GetAppConfigFragmentPayload,
25+
PurgeAppConfigFragmentKey,
1426
PurgeAppConfigFragmentPayload,
1527
SearchAppConfigFragmentsPayload,
1628
UpdateAppConfigFragmentPayload,
@@ -22,17 +34,29 @@
2234
)
2335

2436
__all__ = (
37+
"AdminAppConfigFragmentItemInput",
38+
"AdminBulkCreateAppConfigFragmentsInput",
39+
"AdminBulkCreateAppConfigFragmentsPayload",
40+
"AdminBulkPurgeAppConfigFragmentsInput",
41+
"AdminBulkPurgeAppConfigFragmentsPayload",
42+
"AdminBulkUpdateAppConfigFragmentsInput",
43+
"AdminBulkUpdateAppConfigFragmentsPayload",
44+
"AppConfigFragmentBulkError",
2545
"AppConfigFragmentFilter",
2646
"AppConfigFragmentKeyInput",
2747
"AppConfigFragmentNode",
2848
"AppConfigFragmentOrder",
2949
"AppConfigFragmentOrderField",
3050
"AppConfigScopeType",
51+
"BulkCreateMyAppConfigFragmentsInput",
52+
"BulkUpdateMyAppConfigFragmentsInput",
3153
"CreateAppConfigFragmentInput",
3254
"CreateAppConfigFragmentPayload",
3355
"GetAppConfigFragmentPayload",
56+
"MyAppConfigFragmentItemInput",
3457
"OrderDirection",
3558
"PurgeAppConfigFragmentInput",
59+
"PurgeAppConfigFragmentKey",
3660
"PurgeAppConfigFragmentPayload",
3761
"SearchAppConfigFragmentsInput",
3862
"SearchAppConfigFragmentsPayload",

src/ai/backend/common/dto/manager/v2/app_config_fragment/request.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@
1414
from .types import AppConfigFragmentOrderField, AppConfigScopeType, OrderDirection
1515

1616
__all__ = (
17+
"AdminAppConfigFragmentItemInput",
18+
"AdminBulkCreateAppConfigFragmentsInput",
19+
"AdminBulkPurgeAppConfigFragmentsInput",
20+
"AdminBulkUpdateAppConfigFragmentsInput",
1721
"AppConfigFragmentFilter",
1822
"AppConfigFragmentKeyInput",
1923
"AppConfigFragmentOrder",
24+
"BulkCreateMyAppConfigFragmentsInput",
25+
"BulkUpdateMyAppConfigFragmentsInput",
2026
"CreateAppConfigFragmentInput",
27+
"MyAppConfigFragmentItemInput",
2128
"PurgeAppConfigFragmentInput",
2229
"SearchAppConfigFragmentsInput",
2330
"UpdateAppConfigFragmentInput",
@@ -77,6 +84,51 @@ class AppConfigFragmentOrder(BaseRequestModel):
7784
direction: OrderDirection = Field(default=OrderDirection.ASC, description="Order direction.")
7885

7986

87+
# ── Bulk mutation inputs (BEP-1052 §3) ───────────────────────────
88+
89+
90+
class AdminAppConfigFragmentItemInput(BaseRequestModel):
91+
"""Per-item input for admin bulk create / update (natural key + payload)."""
92+
93+
key: AppConfigFragmentKeyInput = Field(description="Natural-key identifier.")
94+
extra_config: dict[str, Any] = Field(
95+
default_factory=dict,
96+
description="Raw configuration payload (empty dict clears the row).",
97+
)
98+
99+
100+
class AdminBulkCreateAppConfigFragmentsInput(BaseRequestModel):
101+
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to create.")
102+
103+
104+
class AdminBulkUpdateAppConfigFragmentsInput(BaseRequestModel):
105+
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to update.")
106+
107+
108+
class AdminBulkPurgeAppConfigFragmentsInput(BaseRequestModel):
109+
keys: list[AppConfigFragmentKeyInput] = Field(description="Natural keys to purge.")
110+
111+
112+
class MyAppConfigFragmentItemInput(BaseRequestModel):
113+
"""Per-item input for self-service (`my`) bulk — `scope_type`
114+
/ `scope_id` are server-injected, so `name` is the only identifier.
115+
"""
116+
117+
name: str = Field(description="Policy name.")
118+
extra_config: dict[str, Any] = Field(
119+
default_factory=dict,
120+
description="Raw configuration payload (empty dict clears the row).",
121+
)
122+
123+
124+
class BulkCreateMyAppConfigFragmentsInput(BaseRequestModel):
125+
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to create.")
126+
127+
128+
class BulkUpdateMyAppConfigFragmentsInput(BaseRequestModel):
129+
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to update.")
130+
131+
80132
class SearchAppConfigFragmentsInput(BaseRequestModel):
81133
"""Input for searching fragments (raw rows) with filter / order / pagination.
82134

src/ai/backend/common/dto/manager/v2/app_config_fragment/response.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
from .types import AppConfigScopeType
1616

1717
__all__ = (
18+
"AdminBulkCreateAppConfigFragmentsPayload",
19+
"AdminBulkPurgeAppConfigFragmentsPayload",
20+
"AdminBulkUpdateAppConfigFragmentsPayload",
21+
"AppConfigFragmentBulkError",
1822
"AppConfigFragmentNode",
1923
"CreateAppConfigFragmentPayload",
2024
"GetAppConfigFragmentPayload",
25+
"PurgeAppConfigFragmentKey",
2126
"PurgeAppConfigFragmentPayload",
2227
"SearchAppConfigFragmentsPayload",
2328
"UpdateAppConfigFragmentPayload",
@@ -65,6 +70,57 @@ class GetAppConfigFragmentPayload(BaseResponseModel):
6570
item: AppConfigFragmentNode | None = Field(default=None, description="Fragment data, or null.")
6671

6772

73+
# ── Bulk mutation payloads (BEP-1052 §3) ─────────────────────────
74+
75+
76+
class AppConfigFragmentBulkError(BaseResponseModel):
77+
"""Per-item failure information for bulk Fragment mutations."""
78+
79+
index: int = Field(description="Original position in the input list.")
80+
scope_type: AppConfigScopeType = Field(description="Scope type of the failed row.")
81+
scope_id: str = Field(description="Scope id of the failed row.")
82+
name: str = Field(description="Policy name of the failed row.")
83+
message: str = Field(description="Reason for the failure.")
84+
85+
86+
class PurgeAppConfigFragmentKey(BaseResponseModel):
87+
"""Natural-key identifier returned by bulk purge payloads."""
88+
89+
scope_type: AppConfigScopeType = Field(description="Scope type.")
90+
scope_id: str = Field(description="Scope id.")
91+
name: str = Field(description="Policy name.")
92+
93+
94+
class AdminBulkCreateAppConfigFragmentsPayload(BaseResponseModel):
95+
"""Payload for `adminBulkCreateAppConfigFragments`."""
96+
97+
created: list[AppConfigFragmentNode] = Field(description="Created fragments.")
98+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
99+
100+
101+
class AdminBulkUpdateAppConfigFragmentsPayload(BaseResponseModel):
102+
"""Payload for `adminBulkUpdateAppConfigFragments`."""
103+
104+
updated: list[AppConfigFragmentNode] = Field(description="Updated fragments.")
105+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
106+
107+
108+
class AdminBulkPurgeAppConfigFragmentsPayload(BaseResponseModel):
109+
"""Payload for `adminBulkPurgeAppConfigFragments`."""
110+
111+
purged: list[PurgeAppConfigFragmentKey] = Field(
112+
description="Keys of rows actually removed (absent keys are no-oped).",
113+
)
114+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
115+
116+
117+
# `BulkCreateMyAppConfigFragmentsPayload` / `BulkUpdateMyAppConfigFragmentsPayload`
118+
# return recomputed merged `AppConfig` views — they live in
119+
# `common/dto/manager/v2/app_config/response.py` (added with the
120+
# merged-view DTO in the GQL/REST layer) to keep `AppConfigNode` as the
121+
# single source of truth and avoid a circular import.
122+
123+
68124
class SearchAppConfigFragmentsPayload(BaseResponseModel):
69125
"""Payload for paginated fragment search results."""
70126

src/ai/backend/manager/api/adapters/app_config_fragment.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from __future__ import annotations
44

55
from ai.backend.common.dto.manager.v2.app_config_fragment.request import (
6+
AdminBulkCreateAppConfigFragmentsInput,
7+
AdminBulkPurgeAppConfigFragmentsInput,
8+
AdminBulkUpdateAppConfigFragmentsInput,
69
AppConfigFragmentFilter,
710
AppConfigFragmentKeyInput,
811
AppConfigFragmentOrder,
@@ -12,9 +15,14 @@
1215
UpdateAppConfigFragmentInput,
1316
)
1417
from ai.backend.common.dto.manager.v2.app_config_fragment.response import (
18+
AdminBulkCreateAppConfigFragmentsPayload,
19+
AdminBulkPurgeAppConfigFragmentsPayload,
20+
AdminBulkUpdateAppConfigFragmentsPayload,
21+
AppConfigFragmentBulkError,
1522
AppConfigFragmentNode,
1623
CreateAppConfigFragmentPayload,
1724
GetAppConfigFragmentPayload,
25+
PurgeAppConfigFragmentKey,
1826
PurgeAppConfigFragmentPayload,
1927
SearchAppConfigFragmentsPayload,
2028
UpdateAppConfigFragmentPayload,
@@ -24,6 +32,10 @@
2432
)
2533
from ai.backend.common.dto.manager.v2.app_config_fragment.types import OrderDirection
2634
from ai.backend.manager.api.adapter_options.pagination.pagination import PaginationSpec
35+
from ai.backend.manager.data.app_config_fragment.bulk_types import (
36+
AppConfigFragmentBulkItem,
37+
AppConfigFragmentBulkItemError,
38+
)
2739
from ai.backend.manager.data.app_config_fragment.types import (
2840
AppConfigFragmentData,
2941
AppConfigFragmentKey,
@@ -37,6 +49,15 @@
3749
AppConfigFragmentSearchScope,
3850
)
3951
from ai.backend.manager.repositories.base import BatchQuerier, QueryCondition, QueryOrder
52+
from ai.backend.manager.services.app_config_fragment.actions.admin_bulk_create import (
53+
AdminBulkCreateAppConfigFragmentsAction,
54+
)
55+
from ai.backend.manager.services.app_config_fragment.actions.admin_bulk_purge import (
56+
AdminBulkPurgeAppConfigFragmentsAction,
57+
)
58+
from ai.backend.manager.services.app_config_fragment.actions.admin_bulk_update import (
59+
AdminBulkUpdateAppConfigFragmentsAction,
60+
)
4061
from ai.backend.manager.services.app_config_fragment.actions.admin_search import (
4162
AdminSearchAppConfigFragmentsAction,
4263
)
@@ -224,3 +245,70 @@ def _data_to_dto(data: AppConfigFragmentData) -> AppConfigFragmentNode:
224245
created_at=data.created_at,
225246
updated_at=data.updated_at,
226247
)
248+
249+
# ── Bulk mutations (BEP-1052 §3) ───────────────────────────────
250+
251+
async def admin_bulk_create(
252+
self, input: AdminBulkCreateAppConfigFragmentsInput
253+
) -> AdminBulkCreateAppConfigFragmentsPayload:
254+
items = [
255+
AppConfigFragmentBulkItem(
256+
key=self._input_to_key(item.key),
257+
extra_config=dict(item.extra_config),
258+
)
259+
for item in input.items
260+
]
261+
result = await self._processors.app_config_fragment.admin_bulk_create.wait_for_complete(
262+
AdminBulkCreateAppConfigFragmentsAction(items=items)
263+
)
264+
return AdminBulkCreateAppConfigFragmentsPayload(
265+
created=[self._data_to_dto(fragment) for fragment in result.created],
266+
failed=[self._bulk_error_to_dto(err) for err in result.failed],
267+
)
268+
269+
async def admin_bulk_update(
270+
self, input: AdminBulkUpdateAppConfigFragmentsInput
271+
) -> AdminBulkUpdateAppConfigFragmentsPayload:
272+
items = [
273+
AppConfigFragmentBulkItem(
274+
key=self._input_to_key(item.key),
275+
extra_config=dict(item.extra_config),
276+
)
277+
for item in input.items
278+
]
279+
result = await self._processors.app_config_fragment.admin_bulk_update.wait_for_complete(
280+
AdminBulkUpdateAppConfigFragmentsAction(items=items)
281+
)
282+
return AdminBulkUpdateAppConfigFragmentsPayload(
283+
updated=[self._data_to_dto(fragment) for fragment in result.updated],
284+
failed=[self._bulk_error_to_dto(err) for err in result.failed],
285+
)
286+
287+
async def admin_bulk_purge(
288+
self, input: AdminBulkPurgeAppConfigFragmentsInput
289+
) -> AdminBulkPurgeAppConfigFragmentsPayload:
290+
keys = [self._input_to_key(key) for key in input.keys]
291+
result = await self._processors.app_config_fragment.admin_bulk_purge.wait_for_complete(
292+
AdminBulkPurgeAppConfigFragmentsAction(keys=keys)
293+
)
294+
return AdminBulkPurgeAppConfigFragmentsPayload(
295+
purged=[
296+
PurgeAppConfigFragmentKey(
297+
scope_type=DTOAppConfigScopeType(key.scope_type.value),
298+
scope_id=key.scope_id,
299+
name=key.name,
300+
)
301+
for key in result.purged
302+
],
303+
failed=[self._bulk_error_to_dto(err) for err in result.failed],
304+
)
305+
306+
@staticmethod
307+
def _bulk_error_to_dto(err: AppConfigFragmentBulkItemError) -> AppConfigFragmentBulkError:
308+
return AppConfigFragmentBulkError(
309+
index=err.index,
310+
scope_type=DTOAppConfigScopeType(err.scope_type),
311+
scope_id=err.scope_id,
312+
name=err.name,
313+
message=err.message,
314+
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Bulk-mutation service-layer dataclasses for app_config_fragments (BEP-1052 §3)."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Mapping
6+
from dataclasses import dataclass
7+
from typing import Any
8+
9+
from ai.backend.manager.data.app_config_fragment.types import AppConfigFragmentKey
10+
11+
12+
@dataclass(frozen=True)
13+
class AppConfigFragmentBulkItem:
14+
"""One item for `adminBulkCreate/Update` — natural key + payload."""
15+
16+
key: AppConfigFragmentKey
17+
extra_config: Mapping[str, Any]
18+
19+
20+
@dataclass(frozen=True)
21+
class MyAppConfigFragmentBulkItem:
22+
"""One item for `bulkCreate/UpdateMy` — `name` + payload.
23+
24+
`scope_type` is always `USER` and `scope_id` is resolved from the
25+
current user at the adapter layer.
26+
"""
27+
28+
name: str
29+
extra_config: Mapping[str, Any]
30+
31+
32+
@dataclass(frozen=True)
33+
class AppConfigFragmentBulkItemError:
34+
"""Per-item failure carried through bulk action results (BEP-1052 §3).
35+
36+
`scope_type` / `scope_id` / `name` identify which input row failed;
37+
`index` preserves the caller's original list position.
38+
"""
39+
40+
index: int
41+
scope_type: str
42+
scope_id: str
43+
name: str
44+
message: str

0 commit comments

Comments
 (0)