Skip to content

Commit 3b3317c

Browse files
committed
✨ Add ServiceListFilters for service type filtering in paginated service queries
1 parent 9ff7512 commit 3b3317c

File tree

5 files changed

+111
-7
lines changed

5 files changed

+111
-7
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ..boot_options import BootOptions
1010
from ..emails import LowerCaseEmailStr
1111
from ..groups import GroupID
12+
from ..rest_filters import Filters
1213
from ..services_access import ServiceAccessRights, ServiceGroupAccessRightsV2
1314
from ..services_authoring import Author
1415
from ..services_enums import ServiceType
@@ -376,4 +377,13 @@ class MyServiceGet(CatalogOutputSchema):
376377
my_access_rights: ServiceGroupAccessRightsV2
377378

378379

380+
class ServiceListFilters(Filters):
381+
service_type: Annotated[
382+
ServiceType | None,
383+
Field(
384+
description="Filter only services of a given type. If None, then all types are returned"
385+
),
386+
] = None
387+
388+
379389
__all__: tuple[str, ...] = ("ServiceRelease",)

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
# pylint: disable=unused-variable
66

77

8-
from models_library.api_schemas_catalog.services import LatestServiceGet, ServiceGetV2
8+
from models_library.api_schemas_catalog.services import (
9+
LatestServiceGet,
10+
ServiceGetV2,
11+
ServiceListFilters,
12+
)
913
from models_library.api_schemas_webserver.catalog import (
1014
CatalogServiceUpdate,
1115
)
@@ -29,10 +33,12 @@ async def list_services_paginated(
2933
user_id: UserID,
3034
limit: PageLimitInt,
3135
offset: NonNegativeInt,
36+
filters: ServiceListFilters | None = None,
3237
):
3338
assert rpc_client
3439
assert product_name
3540
assert user_id
41+
assert filters is None, "filters not mocked yet"
3642

3743
items = TypeAdapter(list[LatestServiceGet]).validate_python(
3844
LatestServiceGet.model_json_schema()["examples"],
@@ -97,12 +103,14 @@ async def list_my_service_history_paginated(
97103
service_key: ServiceKey,
98104
offset: PageOffsetInt,
99105
limit: PageLimitInt,
106+
filters: ServiceListFilters | None = None,
100107
) -> PageRpc[ServiceRelease]:
101108

102109
assert rpc_client
103110
assert product_name
104111
assert user_id
105112
assert service_key
113+
assert filters is None, "filters not mocked yet"
106114

107115
items = TypeAdapter(list[ServiceRelease]).validate_python(
108116
ServiceRelease.model_json_schema()["examples"],

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
PageRpcLatestServiceGet,
1111
PageRpcServiceRelease,
1212
ServiceGetV2,
13+
ServiceListFilters,
1314
ServiceRelease,
1415
ServiceUpdateV2,
1516
)
@@ -24,10 +25,10 @@
2425
from models_library.services_types import ServiceKey, ServiceVersion
2526
from models_library.users import UserID
2627
from pydantic import TypeAdapter, validate_call
27-
from servicelib.logging_utils import log_decorator
28-
from servicelib.rabbitmq._constants import RPC_REQUEST_DEFAULT_TIMEOUT_S
2928

29+
from ....logging_utils import log_decorator
3030
from ..._client_rpc import RabbitMQRPCClient
31+
from ..._constants import RPC_REQUEST_DEFAULT_TIMEOUT_S
3132

3233
_logger = logging.getLogger(__name__)
3334

@@ -39,6 +40,7 @@ async def list_services_paginated( # pylint: disable=too-many-arguments
3940
user_id: UserID,
4041
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
4142
offset: PageOffsetInt = 0,
43+
filters: ServiceListFilters | None = None,
4244
) -> PageRpcLatestServiceGet:
4345
"""
4446
Raises:
@@ -52,6 +54,7 @@ async def _call(
5254
user_id: UserID,
5355
limit: PageLimitInt,
5456
offset: PageOffsetInt,
57+
filters: ServiceListFilters | None = None,
5558
):
5659
return await rpc_client.request(
5760
CATALOG_RPC_NAMESPACE,
@@ -60,11 +63,16 @@ async def _call(
6063
user_id=user_id,
6164
limit=limit,
6265
offset=offset,
66+
filters=filters,
6367
timeout_s=40 * RPC_REQUEST_DEFAULT_TIMEOUT_S,
6468
)
6569

6670
result = await _call(
67-
product_name=product_name, user_id=user_id, limit=limit, offset=offset
71+
product_name=product_name,
72+
user_id=user_id,
73+
limit=limit,
74+
offset=offset,
75+
filters=filters,
6876
)
6977
assert ( # nosec
7078
TypeAdapter(PageRpc[LatestServiceGet]).validate_python(result) is not None
@@ -249,6 +257,7 @@ async def list_my_service_history_paginated( # pylint: disable=too-many-argumen
249257
service_key: ServiceKey,
250258
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
251259
offset: PageOffsetInt = 0,
260+
filters: ServiceListFilters | None = None,
252261
) -> PageRpcServiceRelease:
253262
"""
254263
Raises:
@@ -262,6 +271,7 @@ async def _call(
262271
service_key: ServiceKey,
263272
limit: PageLimitInt,
264273
offset: PageOffsetInt,
274+
filters: ServiceListFilters | None,
265275
):
266276
return await rpc_client.request(
267277
CATALOG_RPC_NAMESPACE,
@@ -273,6 +283,7 @@ async def _call(
273283
service_key=service_key,
274284
limit=limit,
275285
offset=offset,
286+
filters=filters,
276287
)
277288

278289
result = await _call(
@@ -281,6 +292,7 @@ async def _call(
281292
service_key=service_key,
282293
limit=limit,
283294
offset=offset,
295+
filters=filters,
284296
)
285297

286298
assert ( # nosec

services/catalog/src/simcore_service_catalog/api/rpc/_services.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
PageRpcLatestServiceGet,
99
PageRpcServiceRelease,
1010
ServiceGetV2,
11+
ServiceListFilters,
1112
ServiceUpdateV2,
1213
)
1314
from models_library.products import ProductName
@@ -23,8 +24,9 @@
2324
CatalogForbiddenError,
2425
CatalogItemNotFoundError,
2526
)
26-
from simcore_service_catalog.repository.groups import GroupsRepository
2727

28+
from ...models.services_db import ServiceFiltersDB
29+
from ...repository.groups import GroupsRepository
2830
from ...repository.services import ServicesRepository
2931
from ...service import catalog_services
3032
from .._dependencies.director import get_director_client
@@ -55,6 +57,16 @@ async def _wrapper(app: FastAPI, **kwargs):
5557
return _wrapper
5658

5759

60+
def _type_adapter_to_domain(
61+
filters: ServiceListFilters | None,
62+
) -> ServiceFiltersDB | None:
63+
return (
64+
ServiceFiltersDB.model_validate(filters, from_attributes=True)
65+
if filters
66+
else None
67+
)
68+
69+
5870
@router.expose(reraise_if_error_type=(CatalogForbiddenError, ValidationError))
5971
@_profile_rpc_call
6072
@validate_call(config={"arbitrary_types_allowed": True})
@@ -65,6 +77,7 @@ async def list_services_paginated(
6577
user_id: UserID,
6678
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
6779
offset: PageOffsetInt = 0,
80+
filters: ServiceListFilters | None = None,
6881
) -> PageRpcLatestServiceGet:
6982
assert app.state.engine # nosec
7083

@@ -75,6 +88,7 @@ async def list_services_paginated(
7588
user_id=user_id,
7689
limit=limit,
7790
offset=offset,
91+
filters=_type_adapter_to_domain(filters),
7892
)
7993

8094
assert len(items) <= total_count # nosec
@@ -234,6 +248,7 @@ async def list_my_service_history_paginated(
234248
service_key: ServiceKey,
235249
limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
236250
offset: PageOffsetInt = 0,
251+
filters: ServiceListFilters | None = None,
237252
) -> PageRpcServiceRelease:
238253
assert app.state.engine # nosec
239254

@@ -244,6 +259,7 @@ async def list_my_service_history_paginated(
244259
service_key=service_key,
245260
limit=limit,
246261
offset=offset,
262+
filters=_type_adapter_to_domain(filters),
247263
)
248264

249265
assert len(items) <= total_count # nosec

services/catalog/tests/unit/with_dbs/test_api_rpc.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async def background_sync_task_mocked(
109109
await services_db_tables_injector(fake_data_for_services)
110110

111111

112-
async def test_rpc_catalog_with_no_services_returns_empty_page(
112+
async def test_rpc_list_services_paginated_with_no_services_returns_empty_page(
113113
background_sync_task_mocked: None,
114114
mocked_director_rest_api: MockRouter,
115115
rpc_client: RabbitMQRPCClient,
@@ -484,7 +484,7 @@ async def test_rpc_batch_get_my_services(
484484
assert my_services[1].release.version == other_service_version
485485

486486

487-
async def test_rpc_get_my_service_history(
487+
async def test_rpc_list_my_service_history_paginated(
488488
background_sync_task_mocked: None,
489489
mocked_director_rest_api: MockRouter,
490490
rpc_client: RabbitMQRPCClient,
@@ -553,3 +553,61 @@ async def test_rpc_get_my_service_history(
553553
assert len(release_history) == 2
554554
assert release_history[0].version == service_version_2, "expected newest first"
555555
assert release_history[1].version == service_version_1
556+
557+
558+
async def test_rpc_list_services_paginated_with_filters(
559+
background_sync_task_mocked: None,
560+
mocked_director_rest_api: MockRouter,
561+
rpc_client: RabbitMQRPCClient,
562+
product_name: ProductName,
563+
user_id: UserID,
564+
app: FastAPI,
565+
create_fake_service_data: Callable,
566+
services_db_tables_injector: Callable,
567+
):
568+
assert app
569+
570+
# Create fake services with different types
571+
service_key_1 = "simcore/services/comp/test-filter-service-1"
572+
service_key_2 = "simcore/services/frontend/test-filter-service-2"
573+
service_version = "1.0.0"
574+
575+
fake_services = [
576+
create_fake_service_data(
577+
service_key_1,
578+
service_version,
579+
team_access=None,
580+
everyone_access=None,
581+
product=product_name,
582+
service_type="computational",
583+
),
584+
create_fake_service_data(
585+
service_key_2,
586+
service_version,
587+
team_access=None,
588+
everyone_access=None,
589+
product=product_name,
590+
service_type="frontend",
591+
),
592+
]
593+
594+
# Inject fake services into the database
595+
await services_db_tables_injector(fake_services)
596+
597+
# Apply a filter to match only computational services
598+
filters = {"service_type": "computational"}
599+
page = await list_services_paginated(
600+
rpc_client,
601+
product_name=product_name,
602+
user_id=user_id,
603+
filters=filters,
604+
)
605+
606+
# Validate the response
607+
assert len(page.data) == 1
608+
assert page.data[0].key == service_key_1
609+
assert page.data[0].service_type == "computational"
610+
assert page.meta.total == 1
611+
assert page.meta.count == 1
612+
assert page.links.next is None
613+
assert page.links.prev is None

0 commit comments

Comments
 (0)