Skip to content

Commit bed61ef

Browse files
Merge pull request #1532 from opendatahub-io/main
sync: main to stable
2 parents 848da45 + c628bd1 commit bed61ef

File tree

86 files changed

+5118
-1352
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+5118
-1352
lines changed

api/openapi/catalog.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ paths:
3030
type: string
3131
in: query
3232
required: false
33+
- name: sourceLabel
34+
description: |-
35+
Filter MCP servers by the label associated with the source. Multiple
36+
values can be separated by commas. If one of the values is the
37+
string `null`, then MCP servers from every source without a label will
38+
be returned.
39+
schema:
40+
type: array
41+
items:
42+
type: string
43+
in: query
44+
required: false
3345
- $ref: "#/components/parameters/mcpServerFilterQuery"
3446
- $ref: "#/components/parameters/namedQuery"
3547
- $ref: "#/components/parameters/includeTools"
@@ -326,6 +338,7 @@ paths:
326338
- ModelCatalogService
327339
parameters:
328340
- $ref: "#/components/parameters/name"
341+
- $ref: "#/components/parameters/assetType"
329342
- $ref: "#/components/parameters/pageSize"
330343
- $ref: "#/components/parameters/orderBy"
331344
- $ref: "#/components/parameters/sortOrder"
@@ -2056,6 +2069,13 @@ components:
20562069
maximum: 100
20572070
in: query
20582071
required: false
2072+
assetType:
2073+
name: assetType
2074+
description: Filter sources by asset type.
2075+
schema:
2076+
$ref: "#/components/schemas/CatalogAssetType"
2077+
in: query
2078+
required: false
20592079
artifactType:
20602080
style: form
20612081
explode: true

api/openapi/src/catalog.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ paths:
187187
- ModelCatalogService
188188
parameters:
189189
- $ref: "#/components/parameters/name"
190+
- $ref: "#/components/parameters/assetType"
190191
- $ref: "#/components/parameters/pageSize"
191192
- $ref: "#/components/parameters/orderBy"
192193
- $ref: "#/components/parameters/sortOrder"
@@ -224,6 +225,18 @@ paths:
224225
type: string
225226
in: query
226227
required: false
228+
- name: sourceLabel
229+
description: |-
230+
Filter MCP servers by the label associated with the source. Multiple
231+
values can be separated by commas. If one of the values is the
232+
string `null`, then MCP servers from every source without a label will
233+
be returned.
234+
schema:
235+
type: array
236+
items:
237+
type: string
238+
in: query
239+
required: false
227240
- $ref: "#/components/parameters/mcpServerFilterQuery"
228241
- $ref: "#/components/parameters/namedQuery"
229242
- $ref: "#/components/parameters/includeTools"
@@ -1779,6 +1792,13 @@ components:
17791792
maximum: 100
17801793
in: query
17811794
required: false
1795+
assetType:
1796+
name: assetType
1797+
description: Filter sources by asset type.
1798+
schema:
1799+
$ref: "#/components/schemas/CatalogAssetType"
1800+
in: query
1801+
required: false
17821802
artifactType:
17831803
style: form
17841804
explode: true

catalog/clients/python/src/model_catalog/_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from functools import wraps
1313
from typing import Any, TypeVar
1414
from urllib.parse import quote
15+
from catalog_openapi.models import OrderByField, SortOrder
1516

1617
logger = logging.getLogger(__name__)
1718

@@ -338,7 +339,6 @@ def get_models(
338339
Returns:
339340
Dict with models response.
340341
"""
341-
from catalog_openapi.models import OrderByField, SortOrder
342342

343343
source_list = [source] if source else None
344344
page_size_str = str(page_size) if page_size is not None else None
@@ -370,6 +370,8 @@ def get_artifacts(
370370
model_name: str,
371371
artifact_type: str | list[str] | None = None,
372372
filter_query: str | None = None,
373+
order_by: str | None = None,
374+
sort_order: str | None = None,
373375
page_size: int | None = None,
374376
next_page_token: str | None = None,
375377
) -> dict[str, Any]:
@@ -382,12 +384,16 @@ def get_artifacts(
382384
Accepts "model-artifact", "metrics-artifact", or a list of these values.
383385
Can be a single string or list of strings.
384386
filter_query: Optional filter query.
387+
order_by: Optional field to order by (ID, NAME, CREATE_TIME, LAST_UPDATE_TIME,
388+
or custom property path like "accuracy.double_value").
389+
sort_order: Optional sort order (ASC or DESC).
385390
page_size: Optional page size.
386391
next_page_token: Optional pagination token.
387392
388393
Returns:
389394
Dict with artifacts response.
390395
"""
396+
391397
# Convert artifact_type to list format expected by OpenAPI client
392398
artifact_type_list = None
393399
if artifact_type is not None:
@@ -396,12 +402,18 @@ def get_artifacts(
396402
else:
397403
artifact_type_list = artifact_type
398404

405+
sort_order_enum: SortOrder | None = None
406+
if sort_order:
407+
sort_order_enum = SortOrder(sort_order.upper())
408+
399409
page_size_str = str(page_size) if page_size is not None else None
400410
response = self.catalog_api.get_all_model_artifacts(
401411
source_id=_encode_path_param(source_id),
402412
model_name=_encode_path_param(model_name),
403413
artifact_type=artifact_type_list,
404414
filter_query=filter_query,
415+
order_by=order_by,
416+
sort_order=sort_order_enum,
405417
page_size=page_size_str,
406418
next_page_token=next_page_token,
407419
)

catalog/clients/python/tests/conftest.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -236,46 +236,40 @@ def api_client(user_token: str | None, verify_ssl: bool) -> Generator[CatalogAPI
236236

237237

238238
@pytest.fixture(scope="session")
239-
def model_with_artifacts(api_client: CatalogAPIClient, verify_ssl: bool) -> tuple[str, str]:
239+
def model_with_artifacts(
240+
request: pytest.FixtureRequest, api_client: CatalogAPIClient, verify_ssl: bool
241+
) -> tuple[str, str]:
240242
"""Get a model that has artifacts for testing.
241243
242-
Searches available models to find one with artifacts.
243-
Fails if no models or no models with artifacts are found.
244+
The param value controls the minimum number of artifacts required.
245+
Default is 1. Override via indirect parametrization for tests needing more.
244246
245247
Returns:
246248
Tuple of (source_id, model_name) for a model with artifacts.
247249
248250
Raises:
249-
pytest.fail: If no models are available or no model has artifacts.
251+
pytest.fail: If no models are available or no model has enough artifacts.
250252
"""
253+
min_artifacts = getattr(request, "param", 1)
251254
if not verify_ssl:
252255
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
253256

254257
models = api_client.get_models()
255258
if not models.get("items"):
256259
pytest.fail("No models available - test data may not be loaded")
257260

258-
# Find a model that has artifacts
259261
for model in models["items"]:
260262
source_id = model.get("source_id")
261263
model_name = model.get("name")
262264
if not source_id or not model_name:
263265
continue
264266

265-
# Check if this model has artifacts
266267
artifacts = api_client.get_artifacts(source_id=source_id, model_name=model_name)
267-
if artifacts.get("items"):
268+
items = artifacts.get("items", [])
269+
if len(items) >= min_artifacts:
268270
return source_id, model_name
269271

270-
# Fallback to first model with required fields
271-
model = models["items"][0]
272-
source_id = model.get("source_id")
273-
model_name = model.get("name")
274-
275-
if not source_id or not model_name:
276-
pytest.fail("Model missing source_id or name - test data may be malformed")
277-
278-
return source_id, model_name
272+
pytest.fail(f"No model with at least {min_artifacts} artifact(s) found")
279273

280274

281275
@pytest.fixture(scope="session")

catalog/clients/python/tests/sorting_utils.py

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Utility functions for sorting tests."""
22

3-
import pytest
43
from typing import Any
54

65

@@ -42,25 +41,64 @@ def get_field_value(item: dict[str, Any], field: str) -> Any:
4241
return value
4342

4443

45-
def validate_items_sorted_correctly(items: list[dict], field: str, order: str) -> bool:
46-
"""Verify items are sorted correctly by the specified field.
44+
def sort_items_by_field(items: list[dict], field: str, order: str) -> list[dict]:
45+
"""Sort items by the specified field and order.
4746
4847
Args:
49-
items: List of items to validate
50-
field: Field name to check sorting on (ID, CREATE_TIME, LAST_UPDATE_TIME)
48+
items: List of items to sort
49+
field: Field name to sort on (ID, CREATE_TIME, LAST_UPDATE_TIME)
5150
order: Sort order (ASC or DESC)
5251
5352
Returns:
54-
True if items are sorted correctly, False otherwise
53+
A new list of items sorted by the specified field and order.
54+
55+
Raises:
56+
ValueError: If field or order is invalid
57+
"""
58+
if order in {"ASC", "DESC"}:
59+
return sorted(items, key=lambda item: get_field_value(item, field), reverse=(order == "DESC"))
60+
raise ValueError(f"Invalid sort order: {order}")
61+
62+
63+
64+
def sort_items_by_custom_property(items: list[dict], property_field: str, sort_order: str) -> list[dict]:
65+
"""Sort items by a custom property value with fallback behavior.
66+
67+
Expected behavior:
68+
1. Items WITH the property appear first, sorted by value (ASC/DESC)
69+
2. Items WITHOUT the property appear after, sorted by ID ASC (fallback)
70+
71+
Args:
72+
items: List of artifact items from API response
73+
property_field: Property field path (e.g., "accuracy.double_value")
74+
sort_order: Sort order (ASC or DESC)
75+
76+
Returns:
77+
A new list of items sorted by the custom property with fallback to ID ASC.
78+
79+
Raises:
80+
ValueError: If sort_order is invalid
5581
"""
56-
if len(items) < 2:
57-
pytest.fail("List has fewer than 2 items, double check the data you are passing to this function")
82+
if sort_order not in ("ASC", "DESC"):
83+
raise ValueError(f"Invalid sort order: {sort_order}")
84+
85+
property_name, value_type = property_field.rsplit(".", 1)
86+
87+
items_with = []
88+
items_without = []
89+
90+
for item in items:
91+
custom_props = item.get("customProperties", {})
92+
if property_name in custom_props:
93+
value = custom_props[property_name].get(value_type)
94+
if value is not None:
95+
items_with.append((item, value))
96+
else:
97+
items_without.append(item)
98+
else:
99+
items_without.append(item)
58100

59-
values = [get_field_value(item, field) for item in items]
101+
sorted_with = sorted(items_with, key=lambda x: x[1], reverse=(sort_order == "DESC"))
102+
sorted_without = sorted(items_without, key=lambda item: int(item["id"]))
60103

61-
if order == "ASC":
62-
return all(values[i] <= values[i + 1] for i in range(len(values) - 1))
63-
elif order == "DESC":
64-
return all(values[i] >= values[i + 1] for i in range(len(values) - 1))
65-
else:
66-
raise ValueError(f"Invalid sort order: {order}")
104+
return [item for item, _ in sorted_with] + sorted_without

catalog/clients/python/tests/test_artifacts.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import pytest
1313

1414
from model_catalog import CatalogAPIClient, CatalogValidationError
15+
from tests.sorting_utils import sort_items_by_custom_property, sort_items_by_field
1516

1617

1718
class TestArtifacts:
@@ -428,4 +429,86 @@ def test_filter_artifacts_by_invalid_artifact_type(
428429
source_id=source_id,
429430
model_name=model_name,
430431
artifact_type=invalid_artifact_type,
431-
)
432+
)
433+
434+
435+
@pytest.mark.parametrize("model_with_artifacts", [2], indirect=True)
436+
class TestArtifactsSorting:
437+
"""Basic sorting coverage for artifact endpoints. Thorough testing is done downstream."""
438+
439+
@pytest.mark.parametrize(
440+
"order_by,sort_order",
441+
[
442+
pytest.param("ID", "ASC", id="id_asc"),
443+
pytest.param("ID", "DESC", id="id_desc"),
444+
],
445+
)
446+
def test_artifacts_sorting_by_id(
447+
self,
448+
api_client: CatalogAPIClient,
449+
model_with_artifacts: tuple[str, str],
450+
order_by: str,
451+
sort_order: str,
452+
) -> None:
453+
"""Test artifacts endpoint sorts correctly by ID."""
454+
source_id, model_name = model_with_artifacts
455+
456+
response = api_client.get_artifacts(
457+
source_id=source_id,
458+
model_name=model_name,
459+
order_by=order_by,
460+
sort_order=sort_order,
461+
)
462+
463+
items = response["items"]
464+
assert len(items) >= 2, "Need at least 2 artifacts to validate sorting"
465+
assert items == sort_items_by_field(items, order_by, sort_order)
466+
467+
@pytest.mark.parametrize(
468+
"order_by,sort_order",
469+
[
470+
pytest.param("accuracy.double_value", "ASC", id="accuracy_asc"),
471+
pytest.param("accuracy.double_value", "DESC", id="accuracy_desc"),
472+
],
473+
)
474+
def test_artifacts_sorting_by_custom_property(
475+
self,
476+
api_client: CatalogAPIClient,
477+
model_with_artifacts: tuple[str, str],
478+
order_by: str,
479+
sort_order: str,
480+
) -> None:
481+
"""Test artifacts endpoint sorts correctly by custom property values."""
482+
source_id, model_name = model_with_artifacts
483+
484+
response = api_client.get_artifacts(
485+
source_id=source_id,
486+
model_name=model_name,
487+
order_by=order_by,
488+
sort_order=sort_order,
489+
)
490+
491+
items = response["items"]
492+
assert len(items) >= 2, "Need at least 2 artifacts to validate sorting"
493+
assert items == sort_items_by_custom_property(items, order_by, sort_order)
494+
495+
def test_sorting_by_non_existing_property_falls_back_to_id(
496+
self,
497+
api_client: CatalogAPIClient,
498+
model_with_artifacts: tuple[str, str],
499+
) -> None:
500+
"""Test that sorting by a non-existing property falls back to ID ASC."""
501+
source_id, model_name = model_with_artifacts
502+
503+
response = api_client.get_artifacts(
504+
source_id=source_id,
505+
model_name=model_name,
506+
order_by="non_existing_property.double_value",
507+
sort_order="DESC",
508+
)
509+
510+
items = response["items"]
511+
assert len(items) >= 2, "Need at least 2 artifacts to validate sorting"
512+
513+
# No artifacts have this property, so all should fall back to ID ASC
514+
assert items == sort_items_by_field(items, "ID", "ASC")

0 commit comments

Comments
 (0)