Skip to content

Commit 0ba6902

Browse files
committed
refactor: update custom metadata handling to use NameValueTuple
1 parent 66c9060 commit 0ba6902

File tree

10 files changed

+43
-32
lines changed

10 files changed

+43
-32
lines changed

services/api-server/src/simcore_service_api_server/_service_jobs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from models_library.users import UserID
1616
from pydantic import HttpUrl
1717
from servicelib.logging_utils import log_context
18+
from simcore_service_api_server.models.basic_types import NameValueTuple
1819

1920
from .models.schemas.jobs import Job, JobInputs
2021
from .models.schemas.programs import Program
@@ -41,7 +42,7 @@ async def list_jobs(
4142
self,
4243
job_parent_resource_name: str,
4344
*,
44-
filter_any_custom_metadata: list[dict[str, str]] | None = None,
45+
filter_any_custom_metadata: list[NameValueTuple] | None = None,
4546
pagination_offset: PageOffsetInt = 0,
4647
pagination_limit: PageLimitInt = MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE - 1,
4748
) -> tuple[list[Job], PageMetaInfoLimitOffset]:

services/api-server/src/simcore_service_api_server/_service_solvers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from models_library.services_enums import ServiceType
1313
from models_library.users import UserID
1414
from pydantic import NonNegativeInt, PositiveInt
15+
from simcore_service_api_server.models.basic_types import NameValueTuple
1516

1617
from ._service_jobs import JobService
1718
from ._service_utils import check_user_product_consistency
@@ -92,7 +93,7 @@ async def list_jobs(
9293
*,
9394
filter_by_solver_key: SolverKeyId | None = None,
9495
filter_by_solver_version: VersionStr | None = None,
95-
filter_any_custom_metadata: list[dict[str, str]] | None = None,
96+
filter_any_custom_metadata: list[NameValueTuple] | None = None,
9697
pagination_offset: PageOffsetInt = 0,
9798
pagination_limit: PageLimitInt = DEFAULT_PAGINATION_LIMIT,
9899
) -> tuple[list[Job], PageMetaInfoLimitOffset]:

services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ...exceptions.custom_errors import InsufficientCreditsError, MissingWalletError
2626
from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
2727
from ...models.api_resources import parse_resources_ids
28-
from ...models.basic_types import LogStreamingResponse, VersionStr
28+
from ...models.basic_types import LogStreamingResponse, NameValueTuple, VersionStr
2929
from ...models.domain.files import File as DomainFile
3030
from ...models.pagination import Page, PaginationParams
3131
from ...models.schemas.errors import ErrorGet
@@ -147,7 +147,7 @@ async def list_all_solvers_jobs(
147147
jobs, meta = await solver_service.list_jobs(
148148
filter_any_custom_metadata=(
149149
[
150-
{filter_metadata.name: filter_metadata.pattern}
150+
NameValueTuple(filter_metadata.name, filter_metadata.pattern)
151151
for filter_metadata in filter_job_metadata_params.any
152152
]
153153
if filter_job_metadata_params

services/api-server/src/simcore_service_api_server/models/basic_types.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Annotated, TypeAlias
1+
from typing import Annotated, NamedTuple, TypeAlias
22

33
from fastapi.responses import StreamingResponse
44
from models_library.basic_regex import SIMPLE_VERSION_RE
@@ -13,3 +13,8 @@
1313

1414
class LogStreamingResponse(StreamingResponse):
1515
media_type = "application/x-ndjson"
16+
17+
18+
class NameValueTuple(NamedTuple):
19+
name: str
20+
value: str

services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import (
7171
release_licensed_item_for_wallet as _release_licensed_item_for_wallet,
7272
)
73+
from simcore_service_api_server.models.basic_types import NameValueTuple
7374

7475
from ..exceptions.backend_errors import (
7576
CanNotCheckoutServiceIsNotRunningError,
@@ -250,15 +251,14 @@ async def list_projects_marked_as_jobs(
250251
pagination_offset: int = 0,
251252
pagination_limit: int = 50,
252253
filter_by_job_parent_resource_name_prefix: str | None,
253-
filter_any_custom_metadata: list[dict[str, str]] | None,
254+
filter_any_custom_metadata: list[NameValueTuple] | None,
254255
):
255256
filters = ListProjectsMarkedAsJobRpcFilters(
256257
job_parent_resource_name_prefix=filter_by_job_parent_resource_name_prefix,
257258
any_custom_metadata=(
258259
[
259260
MetadataFilterItem(name=name, pattern=pattern)
260-
for field_match in filter_any_custom_metadata
261-
for name, pattern in field_match.items()
261+
for name, pattern in filter_any_custom_metadata
262262
]
263263
if filter_any_custom_metadata
264264
else None

services/api-server/tests/unit/service/test_service_studies.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ async def test_list_jobs_no_study_id(
2828

2929
# Verify proper prefix was used
3030
assert (
31-
mocked_rpc_client.request.call_args.kwargs["job_parent_resource_name_prefix"]
31+
mocked_rpc_client.request.call_args.kwargs[
32+
"filters"
33+
].job_parent_resource_name_prefix
3234
== "study"
3335
)
3436

@@ -49,7 +51,9 @@ async def test_list_jobs_with_study_id(
4951

5052
# Verify proper prefix was used with study ID
5153
assert (
52-
mocked_rpc_client.request.call_args.kwargs["job_parent_resource_name_prefix"]
54+
mocked_rpc_client.request.call_args.kwargs[
55+
"filters"
56+
].job_parent_resource_name_prefix
5357
== f"study/{study_id}"
5458
)
5559

services/web/server/src/simcore_service_webserver/projects/_controller/projects_rpc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ async def list_projects_marked_as_jobs(
8080
),
8181
filter_any_custom_metadata=(
8282
[
83-
{custom_metadata.name: custom_metadata.pattern}
83+
(custom_metadata.name, custom_metadata.pattern)
8484
for custom_metadata in filters.any_custom_metadata
8585
]
8686
if filters and filters.any_custom_metadata

services/web/server/src/simcore_service_webserver/projects/_jobs_repository.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def _apply_job_parent_resource_name_filter(
3838

3939

4040
def _apply_custom_metadata_filter(
41-
query: sa.sql.Select, any_metadata_fields: list[dict[str, str]]
41+
query: sa.sql.Select, any_metadata_fields: list[tuple[str, str]]
4242
) -> sa.sql.Select:
4343
"""Apply metadata filters to query.
4444
@@ -48,16 +48,13 @@ def _apply_custom_metadata_filter(
4848
assert any_metadata_fields # nosec
4949

5050
metadata_fields_ilike = []
51-
for field in any_metadata_fields:
52-
for key, pattern in field.items():
53-
# Use ->> operator to extract the text value from JSONB
54-
# Then apply ILIKE for case-insensitive pattern matching
55-
sql_pattern = pattern.replace(
56-
"*", "%"
57-
) # Convert glob-like pattern to SQL LIKE
58-
metadata_fields_ilike.append(
59-
projects_metadata.c.custom[key].astext.ilike(sql_pattern)
60-
)
51+
for key, pattern in any_metadata_fields:
52+
# Use ->> operator to extract the text value from JSONB
53+
# Then apply ILIKE for case-insensitive pattern matching
54+
sql_pattern = pattern.replace("*", "%") # Convert glob-like pattern to SQL LIKE
55+
metadata_fields_ilike.append(
56+
projects_metadata.c.custom[key].astext.ilike(sql_pattern)
57+
)
6158

6259
return query.where(sa.or_(*metadata_fields_ilike))
6360

@@ -95,7 +92,7 @@ async def list_projects_marked_as_jobs(
9592
pagination_offset: int,
9693
pagination_limit: int,
9794
filter_by_job_parent_resource_name_prefix: str | None = None,
98-
filter_any_custom_metadata: list[dict[str, str]] | None = None,
95+
filter_any_custom_metadata: list[tuple[str, str]] | None = None,
9996
) -> tuple[int, list[ProjectJobDBGet]]:
10097
"""Lists projects marked as jobs for a specific user and product
10198

services/web/server/src/simcore_service_webserver/projects/_jobs_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async def list_my_projects_marked_as_jobs(
5757
pagination_offset: int = 0,
5858
pagination_limit: int = 10,
5959
filter_by_job_parent_resource_name_prefix: str | None = None,
60-
filter_any_custom_metadata: list[dict[str, str]] | None = None,
60+
filter_any_custom_metadata: list[tuple[str, str]] | None = None,
6161
) -> tuple[int, list[ProjectJobDBGet]]:
6262
"""
6363
Lists paginated projects marked as jobs for the given user and product.

services/web/server/tests/unit/with_dbs/02/test_projects__jobs_service.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ async def test_filter_projects_by_metadata(
237237
)
238238

239239
# 3. Filter by exact metadata
240-
filter_exact = [{"test_key": "test_value"}]
240+
filter_exact = [("test_key", "test_value")]
241241
total_count, result = await list_my_projects_marked_as_jobs(
242242
app=client.app,
243243
product_name=osparc_product_name,
@@ -249,7 +249,10 @@ async def test_filter_projects_by_metadata(
249249
assert result[0].uuid == project_uuid
250250

251251
# 4. Filter by multiple metadata keys in one dict (AND condition)
252-
filter_multiple_keys = [{"category": "simulation", "status": "completed"}]
252+
filter_multiple_keys = [
253+
("category", "simulation"),
254+
("status", "completed"),
255+
]
253256
total_count, result = await list_my_projects_marked_as_jobs(
254257
app=client.app,
255258
product_name=osparc_product_name,
@@ -262,8 +265,8 @@ async def test_filter_projects_by_metadata(
262265

263266
# 5. Filter by alternative metadata (OR condition)
264267
filter_alternative = [
265-
{"status": "completed"},
266-
{"status": "pending"},
268+
("status", "completed"),
269+
("status", "pending"),
267270
]
268271
total_count, result = await list_my_projects_marked_as_jobs(
269272
app=client.app,
@@ -276,7 +279,7 @@ async def test_filter_projects_by_metadata(
276279
assert result[0].uuid == project_uuid
277280

278281
# 6. Filter by non-matching metadata
279-
filter_non_matching = [{"status": "failed"}]
282+
filter_non_matching = [("status", "failed")]
280283
total_count, result = await list_my_projects_marked_as_jobs(
281284
app=client.app,
282285
product_name=osparc_product_name,
@@ -288,7 +291,7 @@ async def test_filter_projects_by_metadata(
288291

289292
# 7. Filter by wildcard pattern (requires SQL LIKE syntax)
290293
# This assumes the implementation supports wildcards in metadata values
291-
filter_wildcard = [{"test_key": "test_*"}]
294+
filter_wildcard = [("test_key", "test_*")]
292295
total_count, result = await list_my_projects_marked_as_jobs(
293296
app=client.app,
294297
product_name=osparc_product_name,
@@ -305,7 +308,7 @@ async def test_filter_projects_by_metadata(
305308
product_name=osparc_product_name,
306309
user_id=user_id,
307310
filter_by_job_parent_resource_name_prefix="test/resource",
308-
filter_any_custom_metadata=[{"category": "simulation"}],
311+
filter_any_custom_metadata=[("category", "simulation")],
309312
)
310313
assert total_count == 1
311314
assert len(result) == 1
@@ -317,7 +320,7 @@ async def test_filter_projects_by_metadata(
317320
product_name=osparc_product_name,
318321
user_id=user_id,
319322
filter_by_job_parent_resource_name_prefix="non-matching",
320-
filter_any_custom_metadata=[{"category": "simulation"}],
323+
filter_any_custom_metadata=[("category", "simulation")],
321324
)
322325
assert total_count == 0
323326
assert len(result) == 0

0 commit comments

Comments
 (0)