Skip to content

Commit 1d5454a

Browse files
authored
🎨 api-server: Add GET /solvers/page Public API Route with Pagination and Filter Support (#7719)
1 parent 6c43a0d commit 1d5454a

File tree

18 files changed

+1427
-191
lines changed

18 files changed

+1427
-191
lines changed

packages/models-library/src/models_library/api_schemas_catalog/services.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,23 +172,57 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
172172
)
173173

174174

175-
class _BaseServiceGetV2(CatalogOutputSchema):
176-
# Model used in catalog's rpc and rest interfaces
175+
class ServiceSummary(CatalogOutputSchema):
177176
key: ServiceKey
178177
version: ServiceVersion
179-
180178
name: str
181-
thumbnail: HttpUrl | None = None
182-
icon: HttpUrl | None = None
183179
description: str
184180

185-
description_ui: bool = False
186-
187181
version_display: str | None = None
188182

183+
contact: LowerCaseEmailStr | None
184+
185+
@staticmethod
186+
def _update_json_schema_extra(schema: JsonDict) -> None:
187+
schema.update(
188+
{
189+
"examples": [
190+
{
191+
"key": _EXAMPLE_SLEEPER["key"],
192+
"version": _EXAMPLE_SLEEPER["version"],
193+
"name": _EXAMPLE_SLEEPER["name"],
194+
"description": _EXAMPLE_SLEEPER["description"],
195+
"version_display": _EXAMPLE_SLEEPER["version_display"],
196+
"contact": _EXAMPLE_SLEEPER["contact"],
197+
},
198+
{
199+
"key": _EXAMPLE_FILEPICKER["key"],
200+
"version": _EXAMPLE_FILEPICKER["version"],
201+
"name": _EXAMPLE_FILEPICKER["name"],
202+
"description": _EXAMPLE_FILEPICKER["description"],
203+
"version_display": None,
204+
"contact": _EXAMPLE_FILEPICKER["contact"],
205+
},
206+
]
207+
}
208+
)
209+
210+
model_config = ConfigDict(
211+
extra="ignore",
212+
populate_by_name=True,
213+
alias_generator=snake_to_camel,
214+
json_schema_extra=_update_json_schema_extra,
215+
)
216+
217+
218+
class _BaseServiceGetV2(ServiceSummary):
189219
service_type: Annotated[ServiceType, Field(alias="type")]
190220

191-
contact: LowerCaseEmailStr | None
221+
thumbnail: HttpUrl | None = None
222+
icon: HttpUrl | None = None
223+
224+
description_ui: bool = False
225+
192226
authors: Annotated[list[Author], Field(min_length=1)]
193227
owner: Annotated[
194228
LowerCaseEmailStr | None,
@@ -217,6 +251,7 @@ class _BaseServiceGetV2(CatalogOutputSchema):
217251
extra="forbid",
218252
populate_by_name=True,
219253
alias_generator=snake_to_camel,
254+
json_schema_extra={"example": _EXAMPLE_SLEEPER},
220255
)
221256

222257

@@ -249,7 +284,6 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
249284

250285

251286
class ServiceGetV2(_BaseServiceGetV2):
252-
# Model used in catalog's rpc and rest interfaces
253287
history: Annotated[
254288
list[ServiceRelease],
255289
Field(
@@ -338,6 +372,9 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
338372
ServiceRelease
339373
]
340374

375+
# Create PageRpc types
376+
PageRpcServiceSummary = PageRpc[ServiceSummary]
377+
341378
ServiceResourcesGet: TypeAlias = ServiceResourcesDict
342379

343380

packages/pytest-simcore/src/pytest_simcore/helpers/catalog_rpc_server.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
LatestServiceGet,
1414
ServiceGetV2,
1515
ServiceListFilters,
16+
ServiceSummary,
1617
ServiceUpdateV2,
1718
)
1819
from models_library.api_schemas_catalog.services_ports import ServicePortGet
@@ -200,6 +201,66 @@ async def get_service_ports(
200201
ServicePortGet.model_json_schema()["examples"],
201202
)
202203

204+
@validate_call(config={"arbitrary_types_allowed": True})
205+
async def list_all_services_summaries_paginated(
206+
self,
207+
rpc_client: RabbitMQRPCClient | MockType,
208+
*,
209+
product_name: ProductName,
210+
user_id: UserID,
211+
limit: PageLimitInt,
212+
offset: NonNegativeInt,
213+
filters: ServiceListFilters | None = None,
214+
):
215+
assert rpc_client
216+
assert product_name
217+
assert user_id
218+
219+
service_summaries = TypeAdapter(list[ServiceSummary]).validate_python(
220+
ServiceSummary.model_json_schema()["examples"],
221+
)
222+
if filters:
223+
filtered_summaries = []
224+
for summary in service_summaries:
225+
# Match service type if specified
226+
if (
227+
filters.service_type
228+
and {
229+
ServiceType.COMPUTATIONAL: "/comp/",
230+
ServiceType.DYNAMIC: "/dynamic/",
231+
}[filters.service_type]
232+
not in summary.key
233+
):
234+
continue
235+
236+
# Match service key pattern if specified
237+
if filters.service_key_pattern and not fnmatch.fnmatch(
238+
summary.key, filters.service_key_pattern
239+
):
240+
continue
241+
242+
# Match version display pattern if specified
243+
if filters.version_display_pattern and (
244+
summary.version_display is None
245+
or not fnmatch.fnmatch(
246+
summary.version_display, filters.version_display_pattern
247+
)
248+
):
249+
continue
250+
251+
filtered_summaries.append(summary)
252+
253+
service_summaries = filtered_summaries
254+
255+
total_count = len(service_summaries)
256+
257+
return PageRpc[ServiceSummary].create(
258+
service_summaries[offset : offset + limit],
259+
total=total_count,
260+
limit=limit,
261+
offset=offset,
262+
)
263+
203264

204265
@dataclass
205266
class ZeroListingCatalogRpcSideEffects:
@@ -216,3 +277,11 @@ async def list_my_service_history_latest_first(self, *args, **kwargs):
216277
limit=10,
217278
offset=0,
218279
)
280+
281+
async def list_all_services_summaries_paginated(self, *args, **kwargs):
282+
return PageRpc[ServiceSummary].create(
283+
[],
284+
total=0,
285+
limit=10,
286+
offset=0,
287+
)

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
MyServiceGet,
1818
PageRpcLatestServiceGet,
1919
PageRpcServiceRelease,
20+
PageRpcServiceSummary,
2021
ServiceGetV2,
2122
ServiceListFilters,
2223
ServiceRelease,
24+
ServiceSummary,
2325
ServiceUpdateV2,
2426
)
2527
from models_library.api_schemas_catalog.services_ports import ServicePortGet
@@ -256,3 +258,51 @@ async def get_service_ports(
256258
TypeAdapter(list[ServicePortGet]).validate_python(result) is not None
257259
) # nosec
258260
return cast(list[ServicePortGet], result)
261+
262+
263+
@validate_call(config={"arbitrary_types_allowed": True})
264+
async def list_all_services_summaries_paginated( # pylint: disable=too-many-arguments
265+
rpc_client: RabbitMQRPCClient,
266+
*,
267+
product_name: ProductName,
268+
user_id: UserID,
269+
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
270+
offset: PageOffsetInt = 0,
271+
filters: ServiceListFilters | None = None,
272+
) -> PageRpcServiceSummary:
273+
"""Lists all services with pagination, including all versions of each service.
274+
275+
Returns a lightweight summary view of services for better performance.
276+
277+
Args:
278+
rpc_client: RPC client instance
279+
product_name: Product name
280+
user_id: User ID
281+
limit: Maximum number of items to return
282+
offset: Number of items to skip
283+
filters: Optional filters to apply
284+
285+
Returns:
286+
Paginated list of all services as summaries
287+
288+
Raises:
289+
ValidationError: on invalid arguments
290+
CatalogForbiddenError: no access-rights to list services
291+
"""
292+
result = await rpc_client.request(
293+
CATALOG_RPC_NAMESPACE,
294+
TypeAdapter(RPCMethodName).validate_python(
295+
list_all_services_summaries_paginated.__name__
296+
),
297+
product_name=product_name,
298+
user_id=user_id,
299+
limit=limit,
300+
offset=offset,
301+
filters=filters,
302+
timeout_s=40 * RPC_REQUEST_DEFAULT_TIMEOUT_S,
303+
)
304+
305+
assert (
306+
TypeAdapter(PageRpc[ServiceSummary]).validate_python(result) is not None
307+
) # nosec
308+
return cast(PageRpc[ServiceSummary], result)

0 commit comments

Comments
 (0)