Skip to content

Commit b0edf43

Browse files
GitHKAndrei Neagu
andauthored
✨ Adding catalog client to dynamic-scheduler ⚠️ (#7162)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent dd342a3 commit b0edf43

File tree

11 files changed

+253
-8
lines changed

11 files changed

+253
-8
lines changed

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

Lines changed: 4 additions & 4 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()
@@ -114,7 +114,7 @@ async def get_service_labels(
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:
@@ -137,7 +137,7 @@ async def get_service_specifications(
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/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,8 @@ services:
563563
REDIS_SECURE: ${REDIS_SECURE}
564564
REDIS_USER: ${REDIS_USER}
565565
REDIS_PASSWORD: ${REDIS_PASSWORD}
566+
CATALOG_HOST: ${CATALOG_HOST}
567+
CATALOG_PORT: ${CATALOG_PORT}
566568
DIRECTOR_V2_HOST: ${DIRECTOR_V2_HOST}
567569
DIRECTOR_V2_PORT: ${DIRECTOR_V2_PORT}
568570
DYNAMIC_SCHEDULER_USE_INTERNAL_SCHEDULER: ${DYNAMIC_SCHEDULER_USE_INTERNAL_SCHEDULER}

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
@@ -24,6 +24,7 @@
2424
from ..api.frontend import initialize_frontend
2525
from ..api.rest.routes import initialize_rest_api
2626
from ..api.rpc.routes import lifespan_rpc_api_routes
27+
from ..services.catalog import lifespan_catalog
2728
from ..services.deferred_manager import lifespan_deferred_manager
2829
from ..services.director_v0 import lifespan_director_v0
2930
from ..services.director_v2 import lifespan_director_v2
@@ -48,6 +49,7 @@ def create_app(settings: ApplicationSettings | None = None) -> FastAPI:
4849
lifespans: list[LifespanGenerator] = [
4950
lifespan_director_v2,
5051
lifespan_director_v0,
52+
lifespan_catalog,
5153
lifespan_rabbitmq,
5254
lifespan_rpc_api_routes,
5355
lifespan_redis,

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.docker_api_proxy import DockerApiProxysettings
@@ -145,6 +146,11 @@ class ApplicationSettings(_BaseApplicationSettings):
145146
description="settings for director-v2 service",
146147
)
147148

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

150156
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 lifespan_catalog
3+
4+
__all__: tuple[str, ...] = (
5+
"CatalogPublicClient",
6+
"lifespan_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 collections.abc import AsyncIterator
2+
3+
from fastapi import FastAPI
4+
from fastapi_lifespan_manager import State
5+
6+
from ._public_client import CatalogPublicClient
7+
from ._thin_client import CatalogThinClient
8+
9+
10+
async def lifespan_catalog(app: FastAPI) -> AsyncIterator[State]:
11+
thin_client = CatalogThinClient(app)
12+
thin_client.set_to_app_state(app)
13+
thin_client.attach_lifespan_to(app)
14+
15+
public_client = CatalogPublicClient(app)
16+
public_client.set_to_app_state(app)
17+
18+
yield {}
19+
20+
CatalogPublicClient.pop_from_app_state(app)
21+
CatalogThinClient.pop_from_app_state(app)
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}")

services/dynamic-scheduler/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"pytest_simcore.docker_swarm",
2626
"pytest_simcore.environment_configs",
2727
"pytest_simcore.faker_projects_data",
28+
"pytest_simcore.faker_users_data",
2829
"pytest_simcore.rabbit_service",
2930
"pytest_simcore.redis_service",
3031
"pytest_simcore.repository_paths",
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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_lifespan: None,
25+
disable_rabbitmq_lifespan: None,
26+
disable_service_tracker_lifespan: None,
27+
disable_deferred_manager_lifespan: None,
28+
disable_notifier_lifespan: None,
29+
disable_status_monitor_lifespan: 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 service_version() -> ServiceVersion:
49+
return "1.0.0"
50+
51+
52+
@pytest.fixture
53+
def service_key() -> ServiceKey:
54+
return "simcore/services/dynamic/test"
55+
56+
57+
@pytest.fixture
58+
def mock_catalog(
59+
app: FastAPI,
60+
user_id: UserID,
61+
service_key: ServiceKey,
62+
service_version: ServiceVersion,
63+
simcore_service_labels: SimcoreServiceLabels,
64+
service_specifications: ServiceSpecifications,
65+
) -> Iterator[None]:
66+
with respx.mock(
67+
base_url=app.state.settings.DYNAMIC_SCHEDULER_CATALOG_SETTINGS.api_base_url,
68+
assert_all_called=False,
69+
assert_all_mocked=True, # IMPORTANT: KEEP always True!
70+
) as respx_mock:
71+
respx_mock.get(
72+
f"/services/{urllib.parse.quote_plus(service_key)}/{service_version}/labels",
73+
name="service labels",
74+
).respond(
75+
status_code=200,
76+
json=simcore_service_labels.model_dump(mode="json"),
77+
)
78+
79+
respx_mock.get(
80+
f"/services/{urllib.parse.quote_plus(service_key)}/{service_version}/specifications?user_id={user_id}",
81+
name="service specifications",
82+
).respond(
83+
status_code=200,
84+
json=service_specifications.model_dump(mode="json"),
85+
)
86+
87+
yield
88+
89+
90+
async def test_get_services_labels(
91+
mock_catalog: None,
92+
app: FastAPI,
93+
service_key: ServiceKey,
94+
service_version: ServiceVersion,
95+
simcore_service_labels: SimcoreServiceLabels,
96+
):
97+
client = CatalogPublicClient.get_from_app_state(app)
98+
result = await client.get_services_labels(service_key, service_version)
99+
assert result.model_dump(mode="json") == simcore_service_labels.model_dump(
100+
mode="json"
101+
)
102+
103+
104+
async def test_get_services_specifications(
105+
mock_catalog: None,
106+
app: FastAPI,
107+
user_id: UserID,
108+
service_key: ServiceKey,
109+
service_version: ServiceVersion,
110+
service_specifications: ServiceSpecifications,
111+
):
112+
client = CatalogPublicClient.get_from_app_state(app)
113+
result = await client.get_services_specifications(
114+
user_id, service_key, service_version
115+
)
116+
assert result.model_dump(mode="json") == service_specifications.model_dump(
117+
mode="json"
118+
)

0 commit comments

Comments
 (0)