Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/11296.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the `AppConfigFragment` service / adapter / DTO surface β€” bulk-only writes (admin + self-service) with partial-success semantics, scope-bound + admin search reads. Layers on top of the foundation in #11282.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from .request import (
AdminAppConfigFragmentItemInput,
AdminBulkCreateAppConfigFragmentsInput,
AdminBulkPurgeAppConfigFragmentsInput,
AdminBulkUpdateAppConfigFragmentsInput,
AppConfigFragmentFilter,
AppConfigFragmentKeyInput,
AppConfigFragmentOrder,
MyAppConfigFragmentItemInput,
MyBulkCreateAppConfigFragmentsInput,
MyBulkUpdateAppConfigFragmentsInput,
SearchAppConfigFragmentsInput,
)
from .response import (
AdminBulkCreateAppConfigFragmentsPayload,
AdminBulkPurgeAppConfigFragmentsPayload,
AdminBulkUpdateAppConfigFragmentsPayload,
AppConfigFragmentBulkError,
AppConfigFragmentNode,
GetAppConfigFragmentPayload,
PurgeAppConfigFragmentKey,
SearchAppConfigFragmentsPayload,
)
from .types import (
AppConfigFragmentOrderField,
AppConfigScopeType,
OrderDirection,
)

__all__ = (
"AdminAppConfigFragmentItemInput",
"AdminBulkCreateAppConfigFragmentsInput",
"AdminBulkCreateAppConfigFragmentsPayload",
"AdminBulkPurgeAppConfigFragmentsInput",
"AdminBulkPurgeAppConfigFragmentsPayload",
"AdminBulkUpdateAppConfigFragmentsInput",
"AdminBulkUpdateAppConfigFragmentsPayload",
"AppConfigFragmentBulkError",
"AppConfigFragmentFilter",
"AppConfigFragmentKeyInput",
"AppConfigFragmentNode",
"AppConfigFragmentOrder",
"AppConfigFragmentOrderField",
"AppConfigScopeType",
"MyBulkCreateAppConfigFragmentsInput",
"MyBulkUpdateAppConfigFragmentsInput",
"GetAppConfigFragmentPayload",
"MyAppConfigFragmentItemInput",
"OrderDirection",
"PurgeAppConfigFragmentKey",
"SearchAppConfigFragmentsInput",
"SearchAppConfigFragmentsPayload",
)
121 changes: 121 additions & 0 deletions src/ai/backend/common/dto/manager/v2/app_config_fragment/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
Request DTOs for app_config_fragment DTO v2.
"""

from __future__ import annotations

from typing import Any

from pydantic import Field

from ai.backend.common.api_handlers import BaseRequestModel
from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter

from .types import AppConfigFragmentOrderField, AppConfigScopeType, OrderDirection

__all__ = (
"AdminAppConfigFragmentItemInput",
"AdminBulkCreateAppConfigFragmentsInput",
"AdminBulkPurgeAppConfigFragmentsInput",
"AdminBulkUpdateAppConfigFragmentsInput",
"AppConfigFragmentFilter",
"AppConfigFragmentKeyInput",
"AppConfigFragmentOrder",
"MyBulkCreateAppConfigFragmentsInput",
"MyBulkUpdateAppConfigFragmentsInput",
"MyAppConfigFragmentItemInput",
"SearchAppConfigFragmentsInput",
)


class AppConfigFragmentKeyInput(BaseRequestModel):
"""Natural-key identifier for a single fragment row."""

scope_type: AppConfigScopeType = Field(description="Scope type.")
scope_id: str = Field(description="Scope id (e.g., domain name, user id, or `public`).")
name: str = Field(
min_length=1,
max_length=128,
description="Policy name.",
)


class AppConfigFragmentFilter(BaseRequestModel):
"""Filter for app-config fragment search."""

id: UUIDFilter | None = Field(default=None, description="Filter by row id.")
name: StringFilter | None = Field(default=None, description="Filter by policy name.")
scope_type: AppConfigScopeType | None = Field(default=None, description="Filter by scope_type.")
scope_id: StringFilter | None = Field(default=None, description="Filter by scope_id.")


class AppConfigFragmentOrder(BaseRequestModel):
"""Order specification for app-config fragments."""

field: AppConfigFragmentOrderField = Field(description="Field to order by.")
direction: OrderDirection = Field(default=OrderDirection.ASC, description="Order direction.")


# ── Bulk mutation inputs ─────────────────────────────────────────


class AdminAppConfigFragmentItemInput(BaseRequestModel):
"""Per-item input for admin bulk create / update (natural key + payload)."""

key: AppConfigFragmentKeyInput = Field(description="Natural-key identifier.")
config: dict[str, Any] = Field(
default_factory=dict,
description="Raw configuration payload (empty dict clears the row).",
)


class AdminBulkCreateAppConfigFragmentsInput(BaseRequestModel):
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to create.")


class AdminBulkUpdateAppConfigFragmentsInput(BaseRequestModel):
items: list[AdminAppConfigFragmentItemInput] = Field(description="Rows to update.")


class AdminBulkPurgeAppConfigFragmentsInput(BaseRequestModel):
keys: list[AppConfigFragmentKeyInput] = Field(description="Natural keys to purge.")


class MyAppConfigFragmentItemInput(BaseRequestModel):
"""Per-item input for self-service (`my`) bulk β€” `scope_type`
/ `scope_id` are server-injected, so `name` is the only identifier.
"""

name: str = Field(description="Policy name.")
config: dict[str, Any] = Field(
default_factory=dict,
description="Raw configuration payload (empty dict clears the row).",
)


class MyBulkCreateAppConfigFragmentsInput(BaseRequestModel):
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to create.")


class MyBulkUpdateAppConfigFragmentsInput(BaseRequestModel):
items: list[MyAppConfigFragmentItemInput] = Field(description="USER-scope rows to update.")


class SearchAppConfigFragmentsInput(BaseRequestModel):
"""Input for searching fragments (raw rows) with filter / order / pagination.

Supports two pagination modes (mutually exclusive):
- Cursor-based: first/after (forward) or last/before (backward)
- Offset-based: limit/offset
"""

filter: AppConfigFragmentFilter | None = Field(default=None, description="Filter conditions.")
order: list[AppConfigFragmentOrder] | None = Field(
default=None, description="Order specifications."
)
first: int | None = Field(default=None, ge=1, description="Number of items from the start.")
after: str | None = Field(default=None, description="Cursor to paginate forward from.")
last: int | None = Field(default=None, ge=1, description="Number of items from the end.")
before: str | None = Field(default=None, description="Cursor to paginate backward from.")
limit: int | None = Field(default=None, ge=1, le=1000, description="Maximum items to return.")
offset: int | None = Field(default=None, ge=0, description="Number of items to skip.")
106 changes: 106 additions & 0 deletions src/ai/backend/common/dto/manager/v2/app_config_fragment/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Response DTOs for app_config_fragment DTO v2.
"""

from __future__ import annotations

from datetime import datetime
from typing import Any
from uuid import UUID

from pydantic import Field

from ai.backend.common.api_handlers import BaseResponseModel

from .types import AppConfigScopeType

__all__ = (
"AdminBulkCreateAppConfigFragmentsPayload",
"AdminBulkPurgeAppConfigFragmentsPayload",
"AdminBulkUpdateAppConfigFragmentsPayload",
"AppConfigFragmentBulkError",
"AppConfigFragmentNode",
"GetAppConfigFragmentPayload",
"PurgeAppConfigFragmentKey",
"SearchAppConfigFragmentsPayload",
)


class AppConfigFragmentNode(BaseResponseModel):
"""Node representing a single fragment row (raw per-scope payload)."""

id: UUID = Field(description="Row ID.")
scope_type: AppConfigScopeType = Field(description="Scope type.")
scope_id: str = Field(description="Scope id.")
name: str = Field(description="Policy name (FK target).")
config: dict[str, Any] | None = Field(
default=None, description="Raw configuration payload, or null."
)
created_at: datetime = Field(description="Creation timestamp.")
updated_at: datetime | None = Field(default=None, description="Last update timestamp.")


class GetAppConfigFragmentPayload(BaseResponseModel):
"""Payload returned after reading a single fragment by natural key."""

item: AppConfigFragmentNode | None = Field(default=None, description="Fragment data, or null.")


class SearchAppConfigFragmentsPayload(BaseResponseModel):
"""Payload for paginated fragment search results."""

items: list[AppConfigFragmentNode] = Field(description="Fragments matching the filter.")
total_count: int = Field(description="Total number of fragments matching the filter.")
has_next_page: bool = Field(default=False, description="Whether there is a next page.")
has_previous_page: bool = Field(default=False, description="Whether there is a previous page.")


# ── Bulk mutation payloads (bulk-only writes) ────────────────────


class AppConfigFragmentBulkError(BaseResponseModel):
"""Per-item failure information for bulk Fragment mutations."""

index: int = Field(description="Original position in the input list.")
scope_type: AppConfigScopeType = Field(description="Scope type of the failed row.")
scope_id: str = Field(description="Scope id of the failed row.")
name: str = Field(description="Policy name of the failed row.")
message: str = Field(description="Reason for the failure.")


class PurgeAppConfigFragmentKey(BaseResponseModel):
"""Natural-key identifier returned by bulk purge payloads."""

scope_type: AppConfigScopeType = Field(description="Scope type.")
scope_id: str = Field(description="Scope id.")
name: str = Field(description="Policy name.")


class AdminBulkCreateAppConfigFragmentsPayload(BaseResponseModel):
"""Payload for `adminBulkCreateAppConfigFragments`."""

created: list[AppConfigFragmentNode] = Field(description="Created fragments.")
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")


class AdminBulkUpdateAppConfigFragmentsPayload(BaseResponseModel):
"""Payload for `adminBulkUpdateAppConfigFragments`."""

updated: list[AppConfigFragmentNode] = Field(description="Updated fragments.")
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")


class AdminBulkPurgeAppConfigFragmentsPayload(BaseResponseModel):
"""Payload for `adminBulkPurgeAppConfigFragments`."""

purged: list[PurgeAppConfigFragmentKey] = Field(
description="Keys of rows actually removed (absent keys are no-oped).",
)
failed: list[AppConfigFragmentBulkError] = Field(description="Per-item failures.")


# `MyBulkCreateAppConfigFragmentsPayload` / `MyBulkUpdateAppConfigFragmentsPayload`
# return recomputed merged `AppConfig` views β€” they live in
# `common/dto/manager/v2/app_config/response.py` (added with the
# merged-view DTO in the GQL/REST layer) to keep `AppConfigNode` as the
# single source of truth and avoid a circular import.
34 changes: 34 additions & 0 deletions src/ai/backend/common/dto/manager/v2/app_config_fragment/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Common types for app_config_fragment DTO v2.
"""

from __future__ import annotations

from enum import StrEnum

from ai.backend.common.dto.manager.v2.common import OrderDirection

__all__ = (
"AppConfigFragmentOrderField",
"AppConfigScopeType",
"OrderDirection",
)


class AppConfigScopeType(StrEnum):
"""Scope types for app-config fragments."""

PUBLIC = "public"
DOMAIN = "domain"
DOMAIN_USER_DEFAULTS = "domain_user_defaults"
USER = "user"


class AppConfigFragmentOrderField(StrEnum):
"""Fields available for ordering app-config fragments."""

SCOPE_TYPE = "scope_type"
SCOPE_ID = "scope_id"
NAME = "name"
CREATED_AT = "created_at"
UPDATED_AT = "updated_at"
Loading
Loading