Skip to content

Commit 554c1a7

Browse files
author
Andrei Neagu
committed
added catalog client to dynamic-scheduler
1 parent 4352166 commit 554c1a7

File tree

9 files changed

+259
-10
lines changed

9 files changed

+259
-10
lines changed

services/director-v2/src/simcore_service_director_v2/modules/catalog.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async def get_service(
8383
) -> dict[str, Any]:
8484
resp = await self.request(
8585
"GET",
86-
f"/services/{urllib.parse.quote( service_key, safe='')}/{service_version}",
86+
f"/services/{urllib.parse.quote(service_key, safe='')}/{service_version}",
8787
params={"user_id": user_id},
8888
headers={"X-Simcore-Products-Name": product_name},
8989
)
@@ -98,7 +98,7 @@ async def get_service_resources(
9898
) -> ServiceResourcesDict:
9999
resp = await self.request(
100100
"GET",
101-
f"/services/{urllib.parse.quote( service_key, safe='')}/{service_version}/resources",
101+
f"/services/{urllib.parse.quote(service_key, safe='')}/{service_version}/resources",
102102
params={"user_id": user_id},
103103
)
104104
resp.raise_for_status()
@@ -109,12 +109,12 @@ async def get_service_resources(
109109
return json_response
110110
raise HTTPException(status_code=resp.status_code, detail=resp.content)
111111

112-
async def get_service_labels(
112+
async def get_service_labels( # to add
113113
self, service_key: ServiceKey, service_version: ServiceVersion
114114
) -> SimcoreServiceLabels:
115115
resp = await self.request(
116116
"GET",
117-
f"/services/{urllib.parse.quote( service_key, safe='')}/{service_version}/labels",
117+
f"/services/{urllib.parse.quote(service_key, safe='')}/{service_version}/labels",
118118
)
119119
resp.raise_for_status()
120120
if resp.status_code == status.HTTP_200_OK:
@@ -132,12 +132,12 @@ async def get_service_extras(
132132
return ServiceExtras.model_validate(resp.json())
133133
raise HTTPException(status_code=resp.status_code, detail=resp.content)
134134

135-
async def get_service_specifications(
135+
async def get_service_specifications( # to add
136136
self, user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion
137137
) -> dict[str, Any]:
138138
resp = await self.request(
139139
"GET",
140-
f"/services/{urllib.parse.quote( service_key, safe='')}/{service_version}/specifications",
140+
f"/services/{urllib.parse.quote(service_key, safe='')}/{service_version}/specifications",
141141
params={"user_id": user_id},
142142
)
143143
resp.raise_for_status()

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/core/application.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from ..api.frontend import setup_frontend
1919
from ..api.rest.routes import setup_rest_api
2020
from ..api.rpc.routes import setup_rpc_api_routes
21+
from ..services.catalog import setup_catalog
2122
from ..services.deferred_manager import setup_deferred_manager
2223
from ..services.director_v0 import setup_director_v0
2324
from ..services.director_v2 import setup_director_v2
@@ -64,6 +65,7 @@ def create_app(settings: ApplicationSettings | None = None) -> FastAPI:
6465

6566
setup_director_v2(app)
6667
setup_director_v0(app)
68+
setup_catalog(app)
6769

6870
setup_rabbitmq(app)
6971
setup_rpc_api_routes(app)

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/core/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from servicelib.logging_utils_filtering import LoggerName, MessageSubstring
66
from settings_library.application import BaseApplicationSettings
77
from settings_library.basic_types import LogLevel, VersionTag
8+
from settings_library.catalog import CatalogSettings
89
from settings_library.director_v0 import DirectorV0Settings
910
from settings_library.director_v2 import DirectorV2Settings
1011
from settings_library.http_client_request import ClientRequestSettings
@@ -144,6 +145,11 @@ class ApplicationSettings(_BaseApplicationSettings):
144145
description="settings for director-v2 service",
145146
)
146147

148+
DYNAMIC_SCHEDULER_CATALOG_SETTINGS: CatalogSettings = Field(
149+
json_schema_extra={"auto_default_from_env": True},
150+
description="settings for catalog service",
151+
)
152+
147153
DYNAMIC_SCHEDULER_PROMETHEUS_INSTRUMENTATION_ENABLED: bool = True
148154

149155
DYNAMIC_SCHEDULER_PROFILING: bool = False
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from ._public_client import CatalogPublicClient
2+
from ._setup import setup_catalog
3+
4+
__all__: tuple[str, ...] = (
5+
"CatalogPublicClient",
6+
"setup_catalog",
7+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from fastapi import FastAPI
2+
from models_library.api_schemas_catalog.services_specifications import (
3+
ServiceSpecifications,
4+
)
5+
from models_library.service_settings_labels import SimcoreServiceLabels
6+
from models_library.services import ServiceKey, ServiceVersion
7+
from models_library.users import UserID
8+
from pydantic import TypeAdapter
9+
from servicelib.fastapi.app_state import SingletonInAppStateMixin
10+
11+
from ._thin_client import CatalogThinClient
12+
13+
14+
class CatalogPublicClient(SingletonInAppStateMixin):
15+
app_state_name: str = "catalog_public_client"
16+
17+
def __init__(self, app: FastAPI) -> None:
18+
self.app = app
19+
20+
async def get_services_labels(
21+
self, service_key: ServiceKey, service_version: ServiceVersion
22+
) -> SimcoreServiceLabels:
23+
response = await CatalogThinClient.get_from_app_state(
24+
self.app
25+
).get_services_labels(service_key, service_version)
26+
return TypeAdapter(SimcoreServiceLabels).validate_python(response.json())
27+
28+
async def get_services_specifications(
29+
self, user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion
30+
) -> ServiceSpecifications:
31+
response = await CatalogThinClient.get_from_app_state(
32+
self.app
33+
).get_services_specifications(user_id, service_key, service_version)
34+
return TypeAdapter(ServiceSpecifications).validate_python(response.json())
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import FastAPI
2+
3+
from ._public_client import CatalogPublicClient
4+
from ._thin_client import CatalogThinClient
5+
6+
7+
def setup_catalog(app: FastAPI) -> None:
8+
async def _on_startup() -> None:
9+
thin_client = CatalogThinClient(app)
10+
thin_client.set_to_app_state(app)
11+
thin_client.attach_lifespan_to(app)
12+
13+
public_client = CatalogPublicClient(app)
14+
public_client.set_to_app_state(app)
15+
16+
async def _on_shutdown() -> None:
17+
CatalogPublicClient.pop_from_app_state(app)
18+
CatalogThinClient.pop_from_app_state(app)
19+
20+
app.add_event_handler("startup", _on_startup)
21+
app.add_event_handler("shutdown", _on_shutdown)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import urllib.parse
2+
3+
from fastapi import FastAPI, status
4+
from httpx import Response
5+
from models_library.services import ServiceKey, ServiceVersion
6+
from models_library.users import UserID
7+
from servicelib.fastapi.app_state import SingletonInAppStateMixin
8+
from servicelib.fastapi.http_client import AttachLifespanMixin
9+
from servicelib.fastapi.http_client_thin import (
10+
BaseThinClient,
11+
expect_status,
12+
retry_on_errors,
13+
)
14+
from yarl import URL
15+
16+
from ...core.settings import ApplicationSettings
17+
18+
19+
class CatalogThinClient(SingletonInAppStateMixin, BaseThinClient, AttachLifespanMixin):
20+
app_state_name: str = "catalog_thin_client"
21+
22+
def __init__(self, app: FastAPI) -> None:
23+
settings: ApplicationSettings = app.state.settings
24+
assert settings.CLIENT_REQUEST.HTTP_CLIENT_REQUEST_TOTAL_TIMEOUT # nosec
25+
26+
super().__init__(
27+
total_retry_interval=int(
28+
settings.CLIENT_REQUEST.HTTP_CLIENT_REQUEST_TOTAL_TIMEOUT
29+
),
30+
extra_allowed_method_names={
31+
"attach_lifespan_to",
32+
"get_from_app_state",
33+
"pop_from_app_state",
34+
"set_to_app_state",
35+
},
36+
base_url=settings.DYNAMIC_SCHEDULER_CATALOG_SETTINGS.api_base_url,
37+
tracing_settings=settings.DYNAMIC_SCHEDULER_TRACING,
38+
)
39+
40+
@retry_on_errors()
41+
@expect_status(status.HTTP_200_OK)
42+
async def get_services_labels(
43+
self, service_key: ServiceKey, service_version: ServiceVersion
44+
) -> Response:
45+
return await self.client.get(
46+
f"/services/{urllib.parse.quote(service_key, safe='')}/{service_version}/labels"
47+
)
48+
49+
@retry_on_errors()
50+
@expect_status(status.HTTP_200_OK)
51+
async def get_services_specifications(
52+
self, user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion
53+
) -> Response:
54+
request_url = URL(
55+
f"/services/{urllib.parse.quote(service_key, safe='')}/{service_version}/specifications",
56+
).with_query(user_id=user_id)
57+
return await self.client.get(f"{request_url}")
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# pylint:disable=redefined-outer-name
2+
# pylint:disable=unused-argument
3+
4+
5+
import urllib.parse
6+
from collections.abc import Iterator
7+
8+
import pytest
9+
import respx
10+
from fastapi import FastAPI
11+
from models_library.api_schemas_catalog.services_specifications import (
12+
ServiceSpecifications,
13+
)
14+
from models_library.service_settings_labels import SimcoreServiceLabels
15+
from models_library.services import ServiceKey, ServiceVersion
16+
from models_library.users import UserID
17+
from pydantic import TypeAdapter
18+
from pytest_simcore.helpers.typing_env import EnvVarsDict
19+
from simcore_service_dynamic_scheduler.services.catalog import CatalogPublicClient
20+
21+
22+
@pytest.fixture
23+
def app_environment(
24+
disable_redis_setup: None,
25+
disable_rabbitmq_setup: None,
26+
disable_service_tracker_setup: None,
27+
disable_deferred_manager_setup: None,
28+
disable_notifier_setup: None,
29+
disable_status_monitor_setup: None,
30+
app_environment: EnvVarsDict,
31+
) -> EnvVarsDict:
32+
return app_environment
33+
34+
35+
@pytest.fixture
36+
def simcore_service_labels() -> SimcoreServiceLabels:
37+
return TypeAdapter(SimcoreServiceLabels).validate_python(
38+
SimcoreServiceLabels.model_json_schema()["examples"][1]
39+
)
40+
41+
42+
@pytest.fixture
43+
def service_specifications() -> ServiceSpecifications:
44+
return TypeAdapter(ServiceSpecifications).validate_python({})
45+
46+
47+
@pytest.fixture
48+
def user_id() -> UserID:
49+
return 1
50+
51+
52+
@pytest.fixture
53+
def service_version() -> ServiceVersion:
54+
return "1.0.0"
55+
56+
57+
@pytest.fixture
58+
def service_key() -> ServiceKey:
59+
return "simcore/services/dynamic/test"
60+
61+
62+
@pytest.fixture
63+
def mock_catalog(
64+
app: FastAPI,
65+
user_id: UserID,
66+
service_key: ServiceKey,
67+
service_version: ServiceVersion,
68+
simcore_service_labels: SimcoreServiceLabels,
69+
service_specifications: ServiceSpecifications,
70+
) -> Iterator[None]:
71+
with respx.mock(
72+
base_url=app.state.settings.DYNAMIC_SCHEDULER_CATALOG_SETTINGS.api_base_url,
73+
assert_all_called=False,
74+
assert_all_mocked=True, # IMPORTANT: KEEP always True!
75+
) as respx_mock:
76+
respx_mock.get(
77+
f"/services/{urllib.parse.quote_plus(service_key)}/{service_version}/labels",
78+
name="service labels",
79+
).respond(
80+
status_code=200,
81+
json=simcore_service_labels.model_dump(mode="json"),
82+
)
83+
84+
respx_mock.get(
85+
f"/services/{urllib.parse.quote_plus(service_key)}/{service_version}/specifications?user_id={user_id}",
86+
name="service specifications",
87+
).respond(
88+
status_code=200,
89+
json=service_specifications.model_dump(mode="json"),
90+
)
91+
92+
print("Respx mock paths:", [route.pattern for route in respx_mock.routes])
93+
94+
yield
95+
96+
97+
async def test_get_services_labels(
98+
mock_catalog: None,
99+
app: FastAPI,
100+
service_key: ServiceKey,
101+
service_version: ServiceVersion,
102+
simcore_service_labels: SimcoreServiceLabels,
103+
):
104+
client = CatalogPublicClient.get_from_app_state(app)
105+
result = await client.get_services_labels(service_key, service_version)
106+
assert result.model_dump(mode="json") == simcore_service_labels.model_dump(
107+
mode="json"
108+
)
109+
110+
111+
async def test_get_services_specifications(
112+
mock_catalog: None,
113+
app: FastAPI,
114+
user_id: UserID,
115+
service_key: ServiceKey,
116+
service_version: ServiceVersion,
117+
service_specifications: ServiceSpecifications,
118+
):
119+
client = CatalogPublicClient.get_from_app_state(app)
120+
result = await client.get_services_specifications(
121+
user_id, service_key, service_version
122+
)
123+
assert result.model_dump(mode="json") == service_specifications.model_dump(
124+
mode="json"
125+
)

services/dynamic-scheduler/tests/unit/test_services_director_v0.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from models_library.api_schemas_directorv2.dynamic_services_service import (
1111
RunningDynamicServiceDetails,
1212
)
13-
from models_library.projects import ProjectID
1413
from models_library.projects_nodes_io import NodeID
1514
from pydantic import TypeAdapter
1615
from pytest_simcore.helpers.typing_env import EnvVarsDict
@@ -33,9 +32,7 @@ def app_environment(
3332

3433

3534
@pytest.fixture
36-
def legacy_service_details(
37-
node_id: NodeID, project_id: ProjectID
38-
) -> RunningDynamicServiceDetails:
35+
def legacy_service_details() -> RunningDynamicServiceDetails:
3936
return TypeAdapter(RunningDynamicServiceDetails).validate_python(
4037
RunningDynamicServiceDetails.model_json_schema()["examples"][0]
4138
)

0 commit comments

Comments
 (0)