Skip to content

Commit 87e336a

Browse files
HyeockJinKimclaude
andauthored
feat(BA-4903): add REST API read endpoints for ResourceSlotType (#9707)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 609e6e3 commit 87e336a

File tree

19 files changed

+798
-2
lines changed

19 files changed

+798
-2
lines changed

changes/9707.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add REST API read endpoints for ResourceSlotType (`GET /resource-slot-types` with pagination/filter/order, `GET /resource-slot-types/{slot_name}` returning 404 on missing)

src/ai/backend/common/data/permission/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class EntityType(enum.StrEnum):
107107
PERMISSION = "permission"
108108
AGENT_RESOURCE = "agent_resource"
109109
RESOURCE_ALLOCATION = "resource_allocation"
110+
RESOURCE_SLOT_TYPE = "resource_slot_type"
110111
RESOURCE_GROUP = "resource_group"
111112
PROMETHEUS_QUERY_PRESET = "prometheus_query_preset"
112113
RESOURCE_PRESET = "resource_preset"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Common DTOs for resource slot type management.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from .request import (
8+
OrderDirection,
9+
ResourceSlotTypeFilter,
10+
ResourceSlotTypeOrder,
11+
ResourceSlotTypeOrderField,
12+
ResourceSlotTypePathParam,
13+
SearchResourceSlotTypesRequest,
14+
)
15+
from .response import (
16+
GetResourceSlotTypeResponse,
17+
NumberFormatDTO,
18+
PaginationInfo,
19+
ResourceSlotTypeDTO,
20+
SearchResourceSlotTypesResponse,
21+
)
22+
23+
__all__ = (
24+
# Request DTOs
25+
"OrderDirection",
26+
"ResourceSlotTypeFilter",
27+
"ResourceSlotTypeOrder",
28+
"ResourceSlotTypeOrderField",
29+
"ResourceSlotTypePathParam",
30+
"SearchResourceSlotTypesRequest",
31+
# Response DTOs
32+
"NumberFormatDTO",
33+
"ResourceSlotTypeDTO",
34+
"PaginationInfo",
35+
"SearchResourceSlotTypesResponse",
36+
"GetResourceSlotTypeResponse",
37+
)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""
2+
Request DTOs for Resource Slot Type REST API.
3+
Shared between Client SDK and Manager API.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from enum import StrEnum
9+
10+
from pydantic import Field
11+
12+
from ai.backend.common.api_handlers import BaseRequestModel
13+
from ai.backend.common.dto.manager.defs import DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT
14+
from ai.backend.common.dto.manager.query import StringFilter
15+
16+
__all__ = (
17+
"OrderDirection",
18+
"ResourceSlotTypeOrderField",
19+
"ResourceSlotTypeFilter",
20+
"ResourceSlotTypeOrder",
21+
"ResourceSlotTypePathParam",
22+
"SearchResourceSlotTypesRequest",
23+
)
24+
25+
26+
class OrderDirection(StrEnum):
27+
"""Order direction for sorting."""
28+
29+
ASC = "asc"
30+
DESC = "desc"
31+
32+
33+
class ResourceSlotTypeOrderField(StrEnum):
34+
"""Fields available for ordering resource slot types."""
35+
36+
SLOT_NAME = "slot_name"
37+
RANK = "rank"
38+
DISPLAY_NAME = "display_name"
39+
40+
41+
class ResourceSlotTypeFilter(BaseRequestModel):
42+
"""Filter conditions for resource slot types."""
43+
44+
slot_name: StringFilter | None = Field(
45+
default=None,
46+
description=(
47+
"Filter by slot name. "
48+
"Supports equals, contains, starts_with, ends_with, "
49+
"and their case-insensitive and negated variants."
50+
),
51+
)
52+
slot_type: StringFilter | None = Field(
53+
default=None,
54+
description="Filter by slot type.",
55+
)
56+
display_name: StringFilter | None = Field(
57+
default=None,
58+
description="Filter by display name.",
59+
)
60+
61+
62+
class ResourceSlotTypeOrder(BaseRequestModel):
63+
"""Order specification for resource slot types."""
64+
65+
field: ResourceSlotTypeOrderField = Field(description="Field to order by")
66+
direction: OrderDirection = Field(default=OrderDirection.ASC, description="Order direction")
67+
68+
69+
class ResourceSlotTypePathParam(BaseRequestModel):
70+
"""Path parameter for resource slot type slot_name."""
71+
72+
slot_name: str = Field(description="Resource slot type name")
73+
74+
75+
class SearchResourceSlotTypesRequest(BaseRequestModel):
76+
"""Request body for searching resource slot types with filters, orders, and pagination."""
77+
78+
filter: ResourceSlotTypeFilter | None = Field(default=None, description="Filter conditions")
79+
order: list[ResourceSlotTypeOrder] | None = Field(
80+
default=None, description="Order specifications"
81+
)
82+
limit: int = Field(
83+
default=DEFAULT_PAGE_LIMIT, ge=1, le=MAX_PAGE_LIMIT, description="Maximum items to return"
84+
)
85+
offset: int = Field(default=0, ge=0, description="Number of items to skip")
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
Response DTOs for Resource Slot Type REST API.
3+
Shared between Client SDK and Manager API.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from pydantic import BaseModel, Field
9+
10+
from ai.backend.common.api_handlers import BaseResponseModel
11+
12+
__all__ = (
13+
"NumberFormatDTO",
14+
"ResourceSlotTypeDTO",
15+
"PaginationInfo",
16+
"SearchResourceSlotTypesResponse",
17+
"GetResourceSlotTypeResponse",
18+
)
19+
20+
21+
class NumberFormatDTO(BaseModel):
22+
"""DTO for number format data."""
23+
24+
binary: bool = Field(description="Whether to use binary (1024-based) units")
25+
round_length: int = Field(description="Number of decimal places to round to")
26+
27+
28+
class ResourceSlotTypeDTO(BaseModel):
29+
"""DTO for resource slot type data."""
30+
31+
slot_name: str = Field(description="Unique slot name identifier")
32+
slot_type: str = Field(description="Slot type (e.g., count, bytes)")
33+
display_name: str = Field(description="Human-readable display name")
34+
description: str = Field(description="Description of the resource slot type")
35+
display_unit: str = Field(description="Unit string for display purposes")
36+
display_icon: str = Field(description="Icon identifier for display purposes")
37+
number_format: NumberFormatDTO = Field(description="Number formatting options")
38+
rank: int = Field(description="Display rank/order")
39+
40+
41+
class PaginationInfo(BaseModel):
42+
"""Pagination information."""
43+
44+
total: int = Field(description="Total number of items")
45+
offset: int = Field(description="Number of items skipped")
46+
limit: int | None = Field(default=None, description="Maximum items returned")
47+
48+
49+
class SearchResourceSlotTypesResponse(BaseResponseModel):
50+
"""Response for searching resource slot types."""
51+
52+
items: list[ResourceSlotTypeDTO] = Field(description="List of resource slot types")
53+
pagination: PaginationInfo = Field(description="Pagination information")
54+
55+
56+
class GetResourceSlotTypeResponse(BaseResponseModel):
57+
"""Response for getting a single resource slot type."""
58+
59+
item: ResourceSlotTypeDTO = Field(description="Resource slot type data")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .registry import register_resource_slot_routes
2+
3+
__all__ = ["register_resource_slot_routes"]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""
2+
Adapter to convert ResourceSlotType DTOs to repository Querier objects.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from ai.backend.common.dto.manager.resource_slot.request import (
8+
OrderDirection,
9+
ResourceSlotTypeFilter,
10+
ResourceSlotTypeOrder,
11+
ResourceSlotTypeOrderField,
12+
SearchResourceSlotTypesRequest,
13+
)
14+
from ai.backend.common.dto.manager.resource_slot.response import (
15+
NumberFormatDTO,
16+
ResourceSlotTypeDTO,
17+
)
18+
from ai.backend.manager.api.rest.adapter import BaseFilterAdapter
19+
from ai.backend.manager.data.resource_slot.types import ResourceSlotTypeData
20+
from ai.backend.manager.repositories.base import (
21+
BatchQuerier,
22+
OffsetPagination,
23+
QueryCondition,
24+
QueryOrder,
25+
)
26+
from ai.backend.manager.repositories.resource_slot.query import QueryConditions, QueryOrders
27+
28+
__all__ = ("ResourceSlotAdapter",)
29+
30+
31+
class ResourceSlotAdapter(BaseFilterAdapter):
32+
"""Adapter for converting resource slot type requests to repository queries."""
33+
34+
def convert_to_dto(self, data: ResourceSlotTypeData) -> ResourceSlotTypeDTO:
35+
"""Convert ResourceSlotTypeData to DTO."""
36+
return ResourceSlotTypeDTO(
37+
slot_name=data.slot_name,
38+
slot_type=data.slot_type,
39+
display_name=data.display_name,
40+
description=data.description,
41+
display_unit=data.display_unit,
42+
display_icon=data.display_icon,
43+
number_format=NumberFormatDTO(
44+
binary=data.number_format.binary,
45+
round_length=data.number_format.round_length,
46+
),
47+
rank=data.rank,
48+
)
49+
50+
def build_querier(self, request: SearchResourceSlotTypesRequest) -> BatchQuerier:
51+
"""Build a BatchQuerier from search request."""
52+
conditions = self._convert_filter(request.filter) if request.filter else []
53+
orders = [self._convert_order(o) for o in request.order] if request.order else []
54+
55+
return BatchQuerier(
56+
conditions=conditions,
57+
orders=orders,
58+
pagination=OffsetPagination(limit=request.limit, offset=request.offset),
59+
)
60+
61+
def _convert_filter(self, filter: ResourceSlotTypeFilter) -> list[QueryCondition]:
62+
"""Convert resource slot type filter to list of query conditions."""
63+
conditions: list[QueryCondition] = []
64+
65+
if filter.slot_name is not None:
66+
condition = self.convert_string_filter(
67+
filter.slot_name,
68+
contains_factory=QueryConditions.by_slot_name_contains,
69+
equals_factory=QueryConditions.by_slot_name_equals,
70+
starts_with_factory=QueryConditions.by_slot_name_starts_with,
71+
ends_with_factory=QueryConditions.by_slot_name_ends_with,
72+
)
73+
if condition is not None:
74+
conditions.append(condition)
75+
76+
if filter.slot_type is not None:
77+
condition = self.convert_string_filter(
78+
filter.slot_type,
79+
contains_factory=QueryConditions.by_slot_type_contains,
80+
equals_factory=QueryConditions.by_slot_type_equals,
81+
starts_with_factory=QueryConditions.by_slot_type_starts_with,
82+
ends_with_factory=QueryConditions.by_slot_type_ends_with,
83+
)
84+
if condition is not None:
85+
conditions.append(condition)
86+
87+
if filter.display_name is not None:
88+
condition = self.convert_string_filter(
89+
filter.display_name,
90+
contains_factory=QueryConditions.by_display_name_contains,
91+
equals_factory=QueryConditions.by_display_name_equals,
92+
starts_with_factory=QueryConditions.by_display_name_starts_with,
93+
ends_with_factory=QueryConditions.by_display_name_ends_with,
94+
)
95+
if condition is not None:
96+
conditions.append(condition)
97+
98+
return conditions
99+
100+
def _convert_order(self, order: ResourceSlotTypeOrder) -> QueryOrder:
101+
"""Convert resource slot type order specification to query order."""
102+
ascending = order.direction == OrderDirection.ASC
103+
104+
if order.field == ResourceSlotTypeOrderField.SLOT_NAME:
105+
return QueryOrders.slot_name(ascending=ascending)
106+
if order.field == ResourceSlotTypeOrderField.RANK:
107+
return QueryOrders.rank(ascending=ascending)
108+
if order.field == ResourceSlotTypeOrderField.DISPLAY_NAME:
109+
return QueryOrders.display_name(ascending=ascending)
110+
raise ValueError(f"Unknown order field: {order.field}")
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Resource Slot Type handler class using constructor dependency injection."""
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.resource_slot.request import (
11+
ResourceSlotTypePathParam,
12+
SearchResourceSlotTypesRequest,
13+
)
14+
from ai.backend.common.dto.manager.resource_slot.response import (
15+
GetResourceSlotTypeResponse,
16+
PaginationInfo,
17+
SearchResourceSlotTypesResponse,
18+
)
19+
from ai.backend.logging import BraceStyleAdapter
20+
from ai.backend.manager.services.resource_slot.actions.get_resource_slot_type import (
21+
GetResourceSlotTypeAction,
22+
)
23+
from ai.backend.manager.services.resource_slot.actions.search_resource_slot_types import (
24+
SearchResourceSlotTypesAction,
25+
)
26+
27+
from .adapter import ResourceSlotAdapter
28+
29+
if TYPE_CHECKING:
30+
from ai.backend.manager.services.resource_slot.processors import ResourceSlotProcessors
31+
32+
log: Final = BraceStyleAdapter(logging.getLogger(__spec__.name))
33+
34+
35+
class ResourceSlotHandler:
36+
"""Resource Slot Type API handler with constructor-injected dependencies."""
37+
38+
def __init__(self, *, resource_slot: ResourceSlotProcessors) -> None:
39+
self._resource_slot = resource_slot
40+
self._adapter = ResourceSlotAdapter()
41+
42+
async def search_resource_slot_types(
43+
self,
44+
body: BodyParam[SearchResourceSlotTypesRequest],
45+
) -> APIResponse:
46+
"""Search resource slot types with filters, orders, and pagination."""
47+
log.info("SEARCH_RESOURCE_SLOT_TYPES")
48+
49+
querier = self._adapter.build_querier(body.parsed)
50+
51+
action_result = await self._resource_slot.search_resource_slot_types.wait_for_complete(
52+
SearchResourceSlotTypesAction(querier=querier)
53+
)
54+
55+
resp = SearchResourceSlotTypesResponse(
56+
items=[self._adapter.convert_to_dto(item) for item in action_result.items],
57+
pagination=PaginationInfo(
58+
total=action_result.total_count,
59+
offset=body.parsed.offset,
60+
limit=body.parsed.limit,
61+
),
62+
)
63+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=resp)
64+
65+
async def get_resource_slot_type(
66+
self,
67+
path: PathParam[ResourceSlotTypePathParam],
68+
) -> APIResponse:
69+
"""Get a single resource slot type by slot_name."""
70+
slot_name = path.parsed.slot_name
71+
log.info("GET_RESOURCE_SLOT_TYPE (slot_name:{})", slot_name)
72+
73+
action_result = await self._resource_slot.get_resource_slot_type.wait_for_complete(
74+
GetResourceSlotTypeAction(slot_name=slot_name)
75+
)
76+
77+
resp = GetResourceSlotTypeResponse(
78+
item=self._adapter.convert_to_dto(action_result.item),
79+
)
80+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=resp)

0 commit comments

Comments
 (0)