Skip to content

Commit 69ccf1f

Browse files
authored
🎨 Exposes get_service_ports to rpc interface of the catalog simcore-service (#7558)
1 parent 9d0e894 commit 69ccf1f

File tree

17 files changed

+573
-129
lines changed

17 files changed

+573
-129
lines changed

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

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Any, Literal
1+
from typing import Annotated, Any, Literal
22

33
from pydantic import BaseModel, ConfigDict, Field
4+
from pydantic.config import JsonDict
45

56
from ..basic_regex import PUBLIC_VARIABLE_NAME_RE
67
from ..services import ServiceInput, ServiceOutput
@@ -10,42 +11,65 @@
1011
update_schema_doc,
1112
)
1213

13-
PortKindStr = Literal["input", "output"]
14-
1514

1615
class ServicePortGet(BaseModel):
17-
key: str = Field(
18-
...,
19-
description="port identifier name",
20-
pattern=PUBLIC_VARIABLE_NAME_RE,
21-
title="Key name",
22-
)
23-
kind: PortKindStr
16+
key: Annotated[
17+
str,
18+
Field(
19+
description="Port identifier name",
20+
pattern=PUBLIC_VARIABLE_NAME_RE,
21+
title="Key name",
22+
),
23+
]
24+
kind: Literal["input", "output"]
2425
content_media_type: str | None = None
25-
content_schema: dict[str, Any] | None = Field(
26-
None,
27-
description="jsonschema for the port's value. SEE https://json-schema.org/understanding-json-schema/",
28-
)
29-
model_config = ConfigDict(
30-
json_schema_extra={
31-
"example": {
32-
"key": "input_1",
33-
"kind": "input",
34-
"content_schema": {
35-
"title": "Sleep interval",
36-
"type": "integer",
37-
"x_unit": "second",
38-
"minimum": 0,
39-
"maximum": 5,
40-
},
41-
}
26+
content_schema: Annotated[
27+
dict[str, Any] | None,
28+
Field(
29+
description="jsonschema for the port's value. SEE https://json-schema.org/understanding-json-schema/",
30+
),
31+
] = None
32+
33+
@staticmethod
34+
def _update_json_schema_extra(schema: JsonDict) -> None:
35+
example_input: dict[str, Any] = {
36+
"key": "input_1",
37+
"kind": "input",
38+
"content_schema": {
39+
"title": "Sleep interval",
40+
"type": "integer",
41+
"x_unit": "second",
42+
"minimum": 0,
43+
"maximum": 5,
44+
},
4245
}
46+
schema.update(
47+
{
48+
"example": example_input,
49+
"examples": [
50+
example_input,
51+
{
52+
"key": "output_1",
53+
"kind": "output",
54+
"content_media_type": "text/plain",
55+
"content_schema": {
56+
"type": "string",
57+
"title": "File containing one random integer",
58+
"description": "Integer is generated in range [1-9]",
59+
},
60+
},
61+
],
62+
}
63+
)
64+
65+
model_config = ConfigDict(
66+
json_schema_extra=_update_json_schema_extra,
4367
)
4468

4569
@classmethod
46-
def from_service_io(
70+
def from_domain_model(
4771
cls,
48-
kind: PortKindStr,
72+
kind: Literal["input", "output"],
4973
key: str,
5074
port: ServiceInput | ServiceOutput,
5175
) -> "ServicePortGet":

packages/models-library/src/models_library/services_io.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ class BaseServiceIOModel(BaseModel):
3030
Base class for service input/outputs
3131
"""
3232

33-
## management
34-
3533
### human readable descriptors
3634
display_order: float | None = Field(
3735
None,

packages/models-library/tests/test_api_schemas_catalog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_service_port_with_file():
2121
}
2222
)
2323

24-
port = ServicePortGet.from_service_io("input", "input_1", io).model_dump(
24+
port = ServicePortGet.from_domain_model("input", "input_1", io).model_dump(
2525
exclude_unset=True
2626
)
2727

@@ -49,7 +49,7 @@ def test_service_port_with_boolean():
4949
}
5050
)
5151

52-
port = ServicePortGet.from_service_io("input", "input_1", io).model_dump(
52+
port = ServicePortGet.from_domain_model("input", "input_1", io).model_dump(
5353
exclude_unset=True
5454
)
5555

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
ServiceGetV2,
1111
ServiceListFilters,
1212
)
13+
from models_library.api_schemas_catalog.services_ports import ServicePortGet
1314
from models_library.api_schemas_webserver.catalog import (
1415
CatalogServiceUpdate,
1516
)
@@ -136,3 +137,22 @@ async def list_my_service_history_paginated(
136137
limit=limit,
137138
offset=offset,
138139
)
140+
141+
async def get_service_ports(
142+
self,
143+
rpc_client: RabbitMQRPCClient,
144+
*,
145+
product_name: ProductName,
146+
user_id: UserID,
147+
service_key: ServiceKey,
148+
service_version: ServiceVersion,
149+
) -> list[ServicePortGet]:
150+
assert rpc_client
151+
assert product_name
152+
assert user_id
153+
assert service_key
154+
assert service_version
155+
156+
return TypeAdapter(list[ServicePortGet]).validate_python(
157+
ServicePortGet.model_json_schema()["examples"],
158+
)

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ServiceRelease,
2323
ServiceUpdateV2,
2424
)
25+
from models_library.api_schemas_catalog.services_ports import ServicePortGet
2526
from models_library.products import ProductName
2627
from models_library.rabbitmq_basic_types import RPCMethodName
2728
from models_library.rest_pagination import PageOffsetInt
@@ -222,3 +223,34 @@ async def list_my_service_history_paginated( # pylint: disable=too-many-argumen
222223
TypeAdapter(PageRpcServiceRelease).validate_python(result) is not None
223224
)
224225
return cast(PageRpc[ServiceRelease], result)
226+
227+
228+
@validate_call(config={"arbitrary_types_allowed": True})
229+
@log_decorator(_logger, level=logging.DEBUG)
230+
async def get_service_ports(
231+
rpc_client: RabbitMQRPCClient,
232+
*,
233+
product_name: ProductName,
234+
user_id: UserID,
235+
service_key: ServiceKey,
236+
service_version: ServiceVersion,
237+
) -> list[ServicePortGet]:
238+
"""Gets service ports (inputs and outputs) for a specific service version
239+
240+
Raises:
241+
ValidationError: on invalid arguments
242+
CatalogItemNotFoundError: service not found in catalog
243+
CatalogForbiddenError: not access rights to read this service
244+
"""
245+
result = await rpc_client.request(
246+
CATALOG_RPC_NAMESPACE,
247+
TypeAdapter(RPCMethodName).validate_python("get_service_ports"),
248+
product_name=product_name,
249+
user_id=user_id,
250+
service_key=service_key,
251+
service_version=service_version,
252+
)
253+
assert (
254+
TypeAdapter(list[ServicePortGet]).validate_python(result) is not None
255+
) # nosec
256+
return cast(list[ServicePortGet], result)

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from fastapi import Depends
55
from models_library.api_schemas_catalog.services import LatestServiceGet, ServiceGetV2
6+
from models_library.api_schemas_catalog.services_ports import ServicePortGet
67
from models_library.products import ProductName
78
from models_library.rest_pagination import (
89
DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
@@ -113,3 +114,33 @@ async def get(
113114
service_key=name,
114115
service_version=version,
115116
)
117+
118+
@_exception_mapper(
119+
rpc_exception_map={
120+
CatalogItemNotFoundError: ProgramOrSolverOrStudyNotFoundError,
121+
CatalogForbiddenError: ServiceForbiddenAccessError,
122+
ValidationError: InvalidInputError,
123+
}
124+
)
125+
async def get_service_ports(
126+
self,
127+
*,
128+
product_name: ProductName,
129+
user_id: UserID,
130+
name: ServiceKey,
131+
version: ServiceVersion,
132+
) -> list[ServicePortGet]:
133+
"""Gets service ports (inputs and outputs) for a specific service version
134+
135+
Raises:
136+
ProgramOrSolverOrStudyNotFoundError: service not found in catalog
137+
ServiceForbiddenAccessError: no access rights to read this service
138+
InvalidInputError: invalid input parameters
139+
"""
140+
return await catalog_rpc.get_service_ports(
141+
self._client,
142+
product_name=product_name,
143+
user_id=user_id,
144+
service_key=name,
145+
service_version=version,
146+
)

services/api-server/tests/unit/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ def get_mock_rabbitmq_rpc_client():
9696
autospec=True,
9797
side_effect=side_effects.list_my_service_history_paginated,
9898
),
99+
"get_service_ports": mocker.patch.object(
100+
catalog_rpc,
101+
"get_service_ports",
102+
autospec=True,
103+
side_effect=side_effects.get_service_ports,
104+
),
99105
}
100106
app.dependency_overrides.pop(get_rabbitmq_rpc_client)
101107

services/api-server/tests/unit/test_services_catalog.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,23 @@ async def test_catalog_service_read_solvers(
7676
assert solver.id == selected_solver.id
7777
assert solver.version == oldest_release.version
7878

79+
# Step 4: Get service ports for the solver
80+
ports = await catalog_service.get_service_ports(
81+
product_name=product_name,
82+
user_id=user_id,
83+
name=selected_solver.id,
84+
version=oldest_release.version,
85+
)
86+
87+
# Verify ports are returned and contain both inputs and outputs
88+
assert ports, "Service ports should not be empty"
89+
assert any(port.kind == "input" for port in ports), "Should contain input ports"
90+
assert any(port.kind == "output" for port in ports), "Should contain output ports"
91+
7992
# checks calls to rpc
8093
mocked_rpc_catalog_service_api["list_services_paginated"].assert_called_once()
8194
mocked_rpc_catalog_service_api[
8295
"list_my_service_history_paginated"
8396
].assert_called_once()
8497
mocked_rpc_catalog_service_api["get_service"].assert_called_once()
98+
mocked_rpc_catalog_service_api["get_service_ports"].assert_called_once()

services/catalog/openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3106,7 +3106,7 @@
31063106
"type": "string",
31073107
"pattern": "^[^_\\W0-9]\\w*$",
31083108
"title": "Key name",
3109-
"description": "port identifier name"
3109+
"description": "Port identifier name"
31103110
},
31113111
"kind": {
31123112
"type": "string",

services/catalog/src/simcore_service_catalog/api/_dependencies/services.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ async def get_service_from_manifest(
9595
return cast(
9696
ServiceMetaDataPublished,
9797
await manifest.get_service(
98+
director_client=director_client,
9899
key=service_key,
99100
version=service_version,
100-
director_client=director_client,
101101
),
102102
)
103103

0 commit comments

Comments
 (0)