Skip to content

Commit 01850b4

Browse files
committed
feat(BA-5836): AppConfigFragment service vertical (service / DTO / adapter, bulk-aware)
Layers the service / adapter / DTO surface on top of the AppConfigFragment foundation that BA-5827 (#11282) lands. Per BEP-1052 §3, all writes are bulk-only (admin + self-service) with partial-success semantics; reads expose `get`, scope-bound `search`, and cross-scope `admin_search`. - v2 DTOs (`common/dto/manager/v2/app_config_fragment/`) — shared by the GQL and REST surfaces in subsequent PRs. - Service + 8 action files + processor package; bulk actions extend `BaseBulkAction[T]` so `BulkActionProcessor` can route per-item validators (RBAC etc. — none wired yet, no-op forward). - Adapter (`api/adapters/app_config_fragment.py`) flesh-out, registered in the adapter registry; `bulk_create_my` / `bulk_update_my` pull `user_id` from `current_user()` instead of accepting it on the action. - Filter `conditions.py` and `orders.py` for typed Fragment search. - `services/factory.py` + `services/processors.py` wiring.
1 parent 7dcf729 commit 01850b4

24 files changed

Lines changed: 1606 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from .request import (
2+
AdminAppConfigFragmentItemInput,
3+
AdminBulkCreateAppConfigFragmentsInput,
4+
AdminBulkPurgeAppConfigFragmentsInput,
5+
AdminBulkUpdateAppConfigFragmentsInput,
6+
AppConfigFragmentFilter,
7+
AppConfigFragmentKeyInput,
8+
AppConfigFragmentOrder,
9+
BulkCreateMyAppConfigFragmentsInput,
10+
BulkUpdateMyAppConfigFragmentsInput,
11+
MyAppConfigFragmentItemInput,
12+
SearchAppConfigFragmentsInput,
13+
)
14+
from .response import (
15+
AdminBulkCreateAppConfigFragmentsPayload,
16+
AdminBulkPurgeAppConfigFragmentsPayload,
17+
AdminBulkUpdateAppConfigFragmentsPayload,
18+
AppConfigFragmentBulkError,
19+
AppConfigFragmentNode,
20+
GetAppConfigFragmentPayload,
21+
PurgeAppConfigFragmentKey,
22+
SearchAppConfigFragmentsPayload,
23+
)
24+
from .types import (
25+
AppConfigFragmentOrderField,
26+
AppConfigScopeType,
27+
OrderDirection,
28+
)
29+
30+
__all__ = (
31+
"AdminAppConfigFragmentItemInput",
32+
"AdminBulkCreateAppConfigFragmentsInput",
33+
"AdminBulkCreateAppConfigFragmentsPayload",
34+
"AdminBulkPurgeAppConfigFragmentsInput",
35+
"AdminBulkPurgeAppConfigFragmentsPayload",
36+
"AdminBulkUpdateAppConfigFragmentsInput",
37+
"AdminBulkUpdateAppConfigFragmentsPayload",
38+
"AppConfigFragmentBulkError",
39+
"AppConfigFragmentFilter",
40+
"AppConfigFragmentKeyInput",
41+
"AppConfigFragmentNode",
42+
"AppConfigFragmentOrder",
43+
"AppConfigFragmentOrderField",
44+
"AppConfigScopeType",
45+
"BulkCreateMyAppConfigFragmentsInput",
46+
"BulkUpdateMyAppConfigFragmentsInput",
47+
"GetAppConfigFragmentPayload",
48+
"MyAppConfigFragmentItemInput",
49+
"OrderDirection",
50+
"PurgeAppConfigFragmentKey",
51+
"SearchAppConfigFragmentsInput",
52+
"SearchAppConfigFragmentsPayload",
53+
)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""
2+
Request DTOs for app_config_fragment DTO v2.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from typing import Any
8+
9+
from pydantic import Field
10+
11+
from ai.backend.common.api_handlers import BaseRequestModel
12+
from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter
13+
14+
from .types import AppConfigFragmentOrderField, AppConfigScopeType, OrderDirection
15+
16+
__all__ = (
17+
"AdminAppConfigFragmentItemInput",
18+
"AdminBulkCreateAppConfigFragmentsInput",
19+
"AdminBulkPurgeAppConfigFragmentsInput",
20+
"AdminBulkUpdateAppConfigFragmentsInput",
21+
"AppConfigFragmentFilter",
22+
"AppConfigFragmentKeyInput",
23+
"AppConfigFragmentOrder",
24+
"BulkCreateMyAppConfigFragmentsInput",
25+
"BulkUpdateMyAppConfigFragmentsInput",
26+
"MyAppConfigFragmentItemInput",
27+
"SearchAppConfigFragmentsInput",
28+
)
29+
30+
31+
class AppConfigFragmentKeyInput(BaseRequestModel):
32+
"""Natural-key identifier for a single fragment row."""
33+
34+
scope_type: AppConfigScopeType = Field(description="Scope type.")
35+
scope_id: str = Field(description="Scope id (e.g., domain name, user id, or `public`).")
36+
name: str = Field(
37+
min_length=1,
38+
max_length=128,
39+
description="Policy name.",
40+
)
41+
42+
43+
class AppConfigFragmentFilter(BaseRequestModel):
44+
"""Filter for app-config fragment search."""
45+
46+
id: UUIDFilter | None = Field(default=None, description="Filter by row id.")
47+
name: StringFilter | None = Field(default=None, description="Filter by policy name.")
48+
scope_type: AppConfigScopeType | None = Field(default=None, description="Filter by scope_type.")
49+
scope_id: StringFilter | None = Field(default=None, description="Filter by scope_id.")
50+
51+
52+
class AppConfigFragmentOrder(BaseRequestModel):
53+
"""Order specification for app-config fragments."""
54+
55+
field: AppConfigFragmentOrderField = Field(description="Field to order by.")
56+
direction: OrderDirection = Field(default=OrderDirection.ASC, description="Order direction.")
57+
58+
59+
# ── Bulk mutation inputs (BEP-1052 §3) ───────────────────────────
60+
61+
62+
class AdminAppConfigFragmentItemInput(BaseRequestModel):
63+
"""Per-item input for admin bulk create / update (natural key + payload)."""
64+
65+
key: AppConfigFragmentKeyInput = Field(description="Natural-key identifier.")
66+
extra_config: dict[str, Any] = Field(
67+
default_factory=dict,
68+
description="Raw configuration payload (empty dict clears the row).",
69+
)
70+
71+
72+
class AdminBulkCreateAppConfigFragmentsInput(BaseRequestModel):
73+
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to create.")
74+
75+
76+
class AdminBulkUpdateAppConfigFragmentsInput(BaseRequestModel):
77+
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to update.")
78+
79+
80+
class AdminBulkPurgeAppConfigFragmentsInput(BaseRequestModel):
81+
keys: list[AppConfigFragmentKeyInput] = Field(description="Natural keys to purge.")
82+
83+
84+
class MyAppConfigFragmentItemInput(BaseRequestModel):
85+
"""Per-item input for self-service (`my`) bulk — `scope_type`
86+
/ `scope_id` are server-injected, so `name` is the only identifier.
87+
"""
88+
89+
name: str = Field(description="Policy name.")
90+
extra_config: dict[str, Any] = Field(
91+
default_factory=dict,
92+
description="Raw configuration payload (empty dict clears the row).",
93+
)
94+
95+
96+
class BulkCreateMyAppConfigFragmentsInput(BaseRequestModel):
97+
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to create.")
98+
99+
100+
class BulkUpdateMyAppConfigFragmentsInput(BaseRequestModel):
101+
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to update.")
102+
103+
104+
class SearchAppConfigFragmentsInput(BaseRequestModel):
105+
"""Input for searching fragments (raw rows) with filter / order / pagination.
106+
107+
Supports two pagination modes (mutually exclusive):
108+
- Cursor-based: first/after (forward) or last/before (backward)
109+
- Offset-based: limit/offset
110+
"""
111+
112+
filter: AppConfigFragmentFilter | None = Field(default=None, description="Filter conditions.")
113+
order: list[AppConfigFragmentOrder] | None = Field(
114+
default=None, description="Order specifications."
115+
)
116+
first: int | None = Field(default=None, ge=1, description="Number of items from the start.")
117+
after: str | None = Field(default=None, description="Cursor to paginate forward from.")
118+
last: int | None = Field(default=None, ge=1, description="Number of items from the end.")
119+
before: str | None = Field(default=None, description="Cursor to paginate backward from.")
120+
limit: int | None = Field(default=None, ge=1, le=1000, description="Maximum items to return.")
121+
offset: int | None = Field(default=None, ge=0, description="Number of items to skip.")
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Response DTOs for app_config_fragment DTO v2.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from datetime import datetime
8+
from typing import Any
9+
from uuid import UUID
10+
11+
from pydantic import Field
12+
13+
from ai.backend.common.api_handlers import BaseResponseModel
14+
15+
from .types import AppConfigScopeType
16+
17+
__all__ = (
18+
"AdminBulkCreateAppConfigFragmentsPayload",
19+
"AdminBulkPurgeAppConfigFragmentsPayload",
20+
"AdminBulkUpdateAppConfigFragmentsPayload",
21+
"AppConfigFragmentBulkError",
22+
"AppConfigFragmentNode",
23+
"GetAppConfigFragmentPayload",
24+
"PurgeAppConfigFragmentKey",
25+
"SearchAppConfigFragmentsPayload",
26+
)
27+
28+
29+
class AppConfigFragmentNode(BaseResponseModel):
30+
"""Node representing a single fragment row (raw per-scope payload)."""
31+
32+
id: UUID = Field(description="Row ID.")
33+
scope_type: AppConfigScopeType = Field(description="Scope type.")
34+
scope_id: str = Field(description="Scope id.")
35+
name: str = Field(description="Policy name (FK target).")
36+
extra_config: dict[str, Any] | None = Field(
37+
default=None, description="Raw configuration payload, or null."
38+
)
39+
created_at: datetime = Field(description="Creation timestamp.")
40+
updated_at: datetime | None = Field(default=None, description="Last update timestamp.")
41+
42+
43+
class GetAppConfigFragmentPayload(BaseResponseModel):
44+
"""Payload returned after reading a single fragment by natural key."""
45+
46+
item: AppConfigFragmentNode | None = Field(default=None, description="Fragment data, or null.")
47+
48+
49+
class SearchAppConfigFragmentsPayload(BaseResponseModel):
50+
"""Payload for paginated fragment search results."""
51+
52+
items: list[AppConfigFragmentNode] = Field(description="Fragments matching the filter.")
53+
total_count: int = Field(description="Total number of fragments matching the filter.")
54+
has_next_page: bool = Field(default=False, description="Whether there is a next page.")
55+
has_previous_page: bool = Field(default=False, description="Whether there is a previous page.")
56+
57+
58+
# ── Bulk mutation payloads (BEP-1052 §3, bulk-only writes) ───────
59+
60+
61+
class AppConfigFragmentBulkError(BaseResponseModel):
62+
"""Per-item failure information for bulk Fragment mutations."""
63+
64+
index: int = Field(description="Original position in the input list.")
65+
scope_type: AppConfigScopeType = Field(description="Scope type of the failed row.")
66+
scope_id: str = Field(description="Scope id of the failed row.")
67+
name: str = Field(description="Policy name of the failed row.")
68+
message: str = Field(description="Reason for the failure.")
69+
70+
71+
class PurgeAppConfigFragmentKey(BaseResponseModel):
72+
"""Natural-key identifier returned by bulk purge payloads."""
73+
74+
scope_type: AppConfigScopeType = Field(description="Scope type.")
75+
scope_id: str = Field(description="Scope id.")
76+
name: str = Field(description="Policy name.")
77+
78+
79+
class AdminBulkCreateAppConfigFragmentsPayload(BaseResponseModel):
80+
"""Payload for `adminBulkCreateAppConfigFragments`."""
81+
82+
created: list[AppConfigFragmentNode] = Field(description="Created fragments.")
83+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
84+
85+
86+
class AdminBulkUpdateAppConfigFragmentsPayload(BaseResponseModel):
87+
"""Payload for `adminBulkUpdateAppConfigFragments`."""
88+
89+
updated: list[AppConfigFragmentNode] = Field(description="Updated fragments.")
90+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
91+
92+
93+
class AdminBulkPurgeAppConfigFragmentsPayload(BaseResponseModel):
94+
"""Payload for `adminBulkPurgeAppConfigFragments`."""
95+
96+
purged: list[PurgeAppConfigFragmentKey] = Field(
97+
description="Keys of rows actually removed (absent keys are no-oped).",
98+
)
99+
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")
100+
101+
102+
# `BulkCreateMyAppConfigFragmentsPayload` / `BulkUpdateMyAppConfigFragmentsPayload`
103+
# return recomputed merged `AppConfig` views — they live in
104+
# `common/dto/manager/v2/app_config/response.py` (added with the
105+
# merged-view DTO in the GQL/REST layer) to keep `AppConfigNode` as the
106+
# single source of truth and avoid a circular import.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Common types for app_config_fragment DTO v2.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from enum import StrEnum
8+
9+
from ai.backend.common.dto.manager.v2.common import OrderDirection
10+
11+
__all__ = (
12+
"AppConfigFragmentOrderField",
13+
"AppConfigScopeType",
14+
"OrderDirection",
15+
)
16+
17+
18+
class AppConfigScopeType(StrEnum):
19+
"""Scope types for app-config fragments (BEP-1052 §1)."""
20+
21+
PUBLIC = "public"
22+
DOMAIN = "domain"
23+
DOMAIN_USER_DEFAULTS = "domain_user_defaults"
24+
USER = "user"
25+
26+
27+
class AppConfigFragmentOrderField(StrEnum):
28+
"""Fields available for ordering app-config fragments."""
29+
30+
SCOPE_TYPE = "scope_type"
31+
SCOPE_ID = "scope_id"
32+
NAME = "name"
33+
CREATED_AT = "created_at"
34+
UPDATED_AT = "updated_at"

0 commit comments

Comments
 (0)