diff --git a/services/catalog/VERSION b/services/catalog/VERSION index 09a3acfa138..bcaffe19b5b 100644 --- a/services/catalog/VERSION +++ b/services/catalog/VERSION @@ -1 +1 @@ -0.6.0 \ No newline at end of file +0.7.0 \ No newline at end of file diff --git a/services/catalog/openapi.json b/services/catalog/openapi.json index ad9422734c8..896399a63b9 100644 --- a/services/catalog/openapi.json +++ b/services/catalog/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "simcore-service-catalog", "description": "Manages and maintains a catalog of all published components (e.g. macro-algorithms, scripts, etc)", - "version": "0.6.0" + "version": "0.7.0" }, "paths": { "/": { @@ -136,6 +136,60 @@ } } }, + "/v0/services/{service_key}/{service_version}/labels": { + "get": { + "tags": [ + "services" + ], + "summary": "Get Service Labels", + "operationId": "get_service_labels_v0_services__service_key___service_version__labels_get", + "parameters": [ + { + "name": "service_key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", + "title": "Service Key" + } + }, + { + "name": "service_version", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Service Version" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Response Get Service Labels V0 Services Service Key Service Version Labels Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/v0/services/{service_key}/{service_version}/specifications": { "get": { "tags": [ diff --git a/services/catalog/setup.cfg b/services/catalog/setup.cfg index ba46449cb68..401431f420a 100644 --- a/services/catalog/setup.cfg +++ b/services/catalog/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.6.0 +current_version = 0.7.0 commit = True message = services/catalog version: {current_version} → {new_version} tag = False diff --git a/services/catalog/src/simcore_service_catalog/api/rest/_services_labels.py b/services/catalog/src/simcore_service_catalog/api/rest/_services_labels.py new file mode 100644 index 00000000000..bf02bb3b90f --- /dev/null +++ b/services/catalog/src/simcore_service_catalog/api/rest/_services_labels.py @@ -0,0 +1,18 @@ +from typing import Annotated, Any + +from fastapi import APIRouter, Depends +from models_library.services import ServiceKey, ServiceVersion + +from ...services.director import DirectorApi +from ..dependencies.director import get_director_api + +router = APIRouter() + + +@router.get("/{service_key:path}/{service_version}/labels") +async def get_service_labels( + service_key: ServiceKey, + service_version: ServiceVersion, + director_client: Annotated[DirectorApi, Depends(get_director_api)], +) -> dict[str, Any]: + return await director_client.get_service_labels(service_key, service_version) diff --git a/services/catalog/src/simcore_service_catalog/api/rest/_services_resources.py b/services/catalog/src/simcore_service_catalog/api/rest/_services_resources.py index 009773aa2de..72080408663 100644 --- a/services/catalog/src/simcore_service_catalog/api/rest/_services_resources.py +++ b/services/catalog/src/simcore_service_catalog/api/rest/_services_resources.py @@ -1,7 +1,7 @@ import logging import urllib.parse from copy import deepcopy -from typing import Annotated, Any, Final, cast +from typing import Annotated, Any, Final import yaml from fastapi import APIRouter, Depends, HTTPException, status @@ -131,12 +131,7 @@ async def _get_service_labels( director_client: DirectorApi, key: ServiceKey, version: ServiceVersion ) -> dict[str, Any] | None: try: - service_labels = cast( - dict[str, Any], - await director_client.get( - f"/services/{urllib.parse.quote_plus(key)}/{version}/labels" - ), - ) + service_labels = await director_client.get_service_labels(key, version) _logger.debug( "received for %s %s", f"/services/{urllib.parse.quote_plus(key)}/{version}/labels", diff --git a/services/catalog/src/simcore_service_catalog/api/rest/routes.py b/services/catalog/src/simcore_service_catalog/api/rest/routes.py index dba2d4e014a..25f0d8e92e5 100644 --- a/services/catalog/src/simcore_service_catalog/api/rest/routes.py +++ b/services/catalog/src/simcore_service_catalog/api/rest/routes.py @@ -7,6 +7,7 @@ _meta, _services, _services_access_rights, + _services_labels, _services_ports, _services_resources, _services_specifications, @@ -38,6 +39,11 @@ tags=_SERVICE_TAGS, prefix=_SERVICE_PREFIX, ) +v0_router.include_router( + _services_labels.router, + tags=_SERVICE_TAGS, + prefix=_SERVICE_PREFIX, +) v0_router.include_router( _services_specifications.router, tags=_SERVICE_TAGS, diff --git a/services/catalog/src/simcore_service_catalog/services/director.py b/services/catalog/src/simcore_service_catalog/services/director.py index 41b975c4e60..8897f2c0a39 100644 --- a/services/catalog/src/simcore_service_catalog/services/director.py +++ b/services/catalog/src/simcore_service_catalog/services/director.py @@ -156,6 +156,17 @@ async def get_service( assert len(data) == 1 # nosec return ServiceMetaDataPublished.model_validate(data[0]) + async def get_service_labels( + self, + service_key: ServiceKey, + service_version: ServiceVersion, + ) -> dict[str, Any]: + response = await self.get( + f"/services/{urllib.parse.quote_plus(service_key)}/{service_version}/labels" + ) + assert isinstance(response, dict) # nosec + return response + async def setup_director( app: FastAPI, tracing_settings: TracingSettings | None diff --git a/services/catalog/tests/unit/conftest.py b/services/catalog/tests/unit/conftest.py index 0bf1cc74f01..59ac3b929dc 100644 --- a/services/catalog/tests/unit/conftest.py +++ b/services/catalog/tests/unit/conftest.py @@ -393,11 +393,41 @@ def mocked_director_service_api_base( yield respx_mock +@pytest.fixture +def get_mocked_service_labels() -> Callable[[str, str], dict]: + def _(service_key: str, service_version: str) -> dict: + return { + "io.simcore.authors": '{"authors": [{"name": "John Smith", "email": "john@acme.com", "affiliation": "ACME\'IS Foundation"}]}', + "io.simcore.contact": '{"contact": "john@acme.com"}', + "io.simcore.description": '{"description": "Autonomous Nervous System Network model"}', + "io.simcore.inputs": '{"inputs": {"input_1": {"displayOrder": 1.0, "label": "Simulation time", "description": "Duration of the simulation", "type": "ref_contentSchema", "contentSchema": {"type": "number", "x_unit": "milli-second"}, "defaultValue": 2.0}}}', + "io.simcore.integration-version": '{"integration-version": "1.0.0"}', + "io.simcore.key": '{"key": "xxxxx"}'.replace("xxxxx", service_key), + "io.simcore.name": '{"name": "Autonomous Nervous System Network model"}', + "io.simcore.outputs": '{"outputs": {"output_1": {"displayOrder": 1.0, "label": "ANS output", "description": "Output of simulation of Autonomous Nervous System Network model", "type": "data:*/*", "fileToKeyMap": {"ANS_output.txt": "output_1"}}, "output_2": {"displayOrder": 2.0, "label": "Stimulation parameters", "description": "stim_param.txt file containing the input provided in the inputs port", "type": "data:*/*", "fileToKeyMap": {"ANS_stim_param.txt": "output_2"}}}}', + "io.simcore.thumbnail": '{"thumbnail": "https://www.statnews.com/wp-content/uploads/2020/05/3D-rat-heart.-iScience--768x432.png"}', + "io.simcore.type": '{"type": "computational"}', + "io.simcore.version": '{"version": "xxxxx"}'.replace( + "xxxxx", service_version + ), + "maintainer": "johnsmith", + "org.label-schema.build-date": "2023-04-17T08:04:15Z", + "org.label-schema.schema-version": "1.0", + "org.label-schema.vcs-ref": "", + "org.label-schema.vcs-url": "", + "simcore.service.restart-policy": "no-restart", + "simcore.service.settings": '[{"name": "Resources", "type": "Resources", "value": {"Limits": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}, "Reservations": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}}}]', + } + + return _ + + @pytest.fixture def mocked_director_service_api( mocked_director_service_api_base: respx.MockRouter, director_service_openapi_specs: dict[str, Any], expected_director_list_services: list[dict[str, Any]], + get_mocked_service_labels: Callable[[str, str], dict], ) -> respx.MockRouter: """ STANDARD fixture to mock director service API @@ -461,30 +491,7 @@ def _get_service_labels(request, service_key, service_version): return httpx.Response( status_code=status.HTTP_200_OK, json={ - "data": { - "io.simcore.authors": '{"authors": [{"name": "John Smith", "email": "john@acme.com", "affiliation": "ACME\'IS Foundation"}]}', - "io.simcore.contact": '{"contact": "john@acme.com"}', - "io.simcore.description": '{"description": "Autonomous Nervous System Network model"}', - "io.simcore.inputs": '{"inputs": {"input_1": {"displayOrder": 1.0, "label": "Simulation time", "description": "Duration of the simulation", "type": "ref_contentSchema", "contentSchema": {"type": "number", "x_unit": "milli-second"}, "defaultValue": 2.0}}}', - "io.simcore.integration-version": '{"integration-version": "1.0.0"}', - "io.simcore.key": '{"key": "xxxxx"}'.replace( - "xxxxx", found["key"] - ), - "io.simcore.name": '{"name": "Autonomous Nervous System Network model"}', - "io.simcore.outputs": '{"outputs": {"output_1": {"displayOrder": 1.0, "label": "ANS output", "description": "Output of simulation of Autonomous Nervous System Network model", "type": "data:*/*", "fileToKeyMap": {"ANS_output.txt": "output_1"}}, "output_2": {"displayOrder": 2.0, "label": "Stimulation parameters", "description": "stim_param.txt file containing the input provided in the inputs port", "type": "data:*/*", "fileToKeyMap": {"ANS_stim_param.txt": "output_2"}}}}', - "io.simcore.thumbnail": '{"thumbnail": "https://www.statnews.com/wp-content/uploads/2020/05/3D-rat-heart.-iScience--768x432.png"}', - "io.simcore.type": '{"type": "computational"}', - "io.simcore.version": '{"version": "xxxxx"}'.replace( - "xxxxx", found["version"] - ), - "maintainer": "iavarone", - "org.label-schema.build-date": "2023-04-17T08:04:15Z", - "org.label-schema.schema-version": "1.0", - "org.label-schema.vcs-ref": "", - "org.label-schema.vcs-url": "", - "simcore.service.restart-policy": "no-restart", - "simcore.service.settings": '[{"name": "Resources", "type": "Resources", "value": {"Limits": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}, "Reservations": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}}}]', - } + "data": get_mocked_service_labels(found["key"], found["version"]) }, ) return httpx.Response( diff --git a/services/catalog/tests/unit/test_utils_service_labels.py b/services/catalog/tests/unit/test_utils_service_labels.py new file mode 100644 index 00000000000..950a30cbd89 --- /dev/null +++ b/services/catalog/tests/unit/test_utils_service_labels.py @@ -0,0 +1,31 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + +from collections.abc import Callable +from unittest.mock import AsyncMock + +import pytest +from fastapi import FastAPI +from httpx import AsyncClient +from respx import MockRouter + + +@pytest.fixture +def mock_engine(app: FastAPI) -> None: + app.state.engine = AsyncMock() + + +async def test_get_service_labels( + postgres_setup_disabled: None, + mocked_director_service_api: MockRouter, + rabbitmq_and_rpc_setup_disabled: None, + background_tasks_setup_disabled: None, + mock_engine: None, + get_mocked_service_labels: Callable[[str, str], dict], + aclient: AsyncClient, +): + service_key = "simcore/services/comp/ans-model" + service_version = "3.0.0" + result = await aclient.get(f"/v0/services/{service_key}/{service_version}/labels") + assert result.status_code == 200, result.text + assert result.json() == get_mocked_service_labels(service_key, service_version) diff --git a/services/catalog/tests/unit/with_dbs/test_api_rest_services_resources.py b/services/catalog/tests/unit/with_dbs/test_api_rest_services_resources.py index d9ef5ea328f..06bc37b97d6 100644 --- a/services/catalog/tests/unit/with_dbs/test_api_rest_services_resources.py +++ b/services/catalog/tests/unit/with_dbs/test_api_rest_services_resources.py @@ -186,7 +186,7 @@ class _ServiceResourceParams: ], ) async def test_get_service_resources( - background_tasks_setup_disabled, + background_tasks_setup_disabled: None, rabbitmq_and_rpc_setup_disabled: None, mocked_director_service_labels: Route, client: TestClient, @@ -298,7 +298,7 @@ def factory(services_labels: dict[str, dict[str, Any]]) -> None: ], ) async def test_get_service_resources_sim4life_case( - background_tasks_setup_disabled, + background_tasks_setup_disabled: None, rabbitmq_and_rpc_setup_disabled: None, create_mock_director_service_labels: Callable, client: TestClient, @@ -319,7 +319,7 @@ async def test_get_service_resources_sim4life_case( async def test_get_service_resources_raises_errors( - background_tasks_setup_disabled, + background_tasks_setup_disabled: None, rabbitmq_and_rpc_setup_disabled: None, mocked_director_service_labels: Route, client: TestClient, diff --git a/services/catalog/tests/unit/with_dbs/test_services_services_api.py b/services/catalog/tests/unit/with_dbs/test_services_services_api.py index 63f0fd06e56..bcfae48d319 100644 --- a/services/catalog/tests/unit/with_dbs/test_services_services_api.py +++ b/services/catalog/tests/unit/with_dbs/test_services_services_api.py @@ -88,7 +88,7 @@ async def director_client(app: FastAPI) -> DirectorApi: # ensures manifest API cache is reset assert hasattr(manifest.get_service, "cache") - assert manifest.get_service.cache.clear() + assert await manifest.get_service.cache.clear() return director_api diff --git a/services/director-v2/src/simcore_service_director_v2/api/routes/dynamic_services.py b/services/director-v2/src/simcore_service_director_v2/api/routes/dynamic_services.py index 24db21cbd23..c7562ccf5cd 100644 --- a/services/director-v2/src/simcore_service_director_v2/api/routes/dynamic_services.py +++ b/services/director-v2/src/simcore_service_director_v2/api/routes/dynamic_services.py @@ -17,7 +17,6 @@ from models_library.projects import ProjectAtDB, ProjectID from models_library.projects_nodes_io import NodeID from models_library.service_settings_labels import SimcoreServiceLabels -from models_library.services import ServiceKeyVersion from models_library.users import UserID from pydantic import NonNegativeFloat, NonNegativeInt from servicelib.fastapi.requests_decorators import cancel_on_disconnect @@ -32,11 +31,13 @@ from tenacity.stop import stop_after_delay from tenacity.wait import wait_fixed +from ...api.dependencies.catalog import get_catalog_client from ...api.dependencies.database import get_repository from ...api.dependencies.rabbitmq import get_rabbitmq_client_from_request from ...core.dynamic_services_settings import DynamicServicesSettings from ...core.dynamic_services_settings.scheduler import DynamicServicesSchedulerSettings from ...modules import projects_networks +from ...modules.catalog import CatalogClient from ...modules.db.repositories.projects import ProjectsRepository from ...modules.db.repositories.projects_networks import ProjectsNetworksRepository from ...modules.director_v0 import DirectorV0Client @@ -104,6 +105,7 @@ async def list_tracked_dynamic_services( @log_decorator(logger=logger) async def create_dynamic_service( service: DynamicServiceCreate, + catalog_client: Annotated[CatalogClient, Depends(get_catalog_client)], director_v0_client: Annotated[DirectorV0Client, Depends(get_director_v0_client)], dynamic_services_settings: Annotated[ DynamicServicesSettings, Depends(get_dynamic_services_settings) @@ -114,9 +116,7 @@ async def create_dynamic_service( x_simcore_user_agent: str = Header(...), ) -> DynamicServiceGet | RedirectResponse: simcore_service_labels: SimcoreServiceLabels = ( - await director_v0_client.get_service_labels( - service=ServiceKeyVersion(key=service.key, version=service.version) - ) + await catalog_client.get_service_labels(service.key, service.version) ) # LEGACY (backwards compatibility) @@ -324,7 +324,7 @@ async def update_projects_networks( ProjectsRepository, Depends(get_repository(ProjectsRepository)) ], scheduler: Annotated[DynamicSidecarsScheduler, Depends(get_scheduler)], - director_v0_client: Annotated[DirectorV0Client, Depends(get_director_v0_client)], + catalog_client: Annotated[CatalogClient, Depends(get_catalog_client)], rabbitmq_client: Annotated[ RabbitMQClient, Depends(get_rabbitmq_client_from_request) ], @@ -334,7 +334,7 @@ async def update_projects_networks( projects_networks_repository=projects_networks_repository, projects_repository=projects_repository, scheduler=scheduler, - director_v0_client=director_v0_client, + catalog_client=catalog_client, rabbitmq_client=rabbitmq_client, project_id=project_id, ) diff --git a/services/director-v2/src/simcore_service_director_v2/cli/_core.py b/services/director-v2/src/simcore_service_director_v2/cli/_core.py index 67a99d4886f..bc7cc095898 100644 --- a/services/director-v2/src/simcore_service_director_v2/cli/_core.py +++ b/services/director-v2/src/simcore_service_director_v2/cli/_core.py @@ -16,6 +16,7 @@ from rich.live import Live from rich.table import Table from servicelib.services_utils import get_service_from_key +from simcore_service_director_v2.modules.catalog import CatalogClient from tenacity.asyncio import AsyncRetrying from tenacity.stop import stop_after_attempt from tenacity.wait import wait_random_exponential @@ -25,7 +26,6 @@ from ..models.dynamic_services_scheduler import DynamicSidecarNamesHelper from ..modules import db, director_v0, dynamic_sidecar from ..modules.db.repositories.projects import ProjectsRepository -from ..modules.director_v0 import DirectorV0Client from ..modules.dynamic_sidecar import api_client from ..modules.projects_networks import requires_dynamic_sidecar from ..utils.db import get_repository @@ -101,7 +101,7 @@ async def async_project_save_state(project_id: ProjectID, save_attempts: int) -> if not await requires_dynamic_sidecar( service_key=node_content.key, service_version=node_content.version, - director_v0_client=DirectorV0Client.instance(app), + catalog_client=CatalogClient.instance(app), ): continue diff --git a/services/director-v2/src/simcore_service_director_v2/modules/catalog.py b/services/director-v2/src/simcore_service_director_v2/modules/catalog.py index 6125c4cfb02..eb9eead1e9e 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/catalog.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/catalog.py @@ -5,6 +5,7 @@ import httpx from fastapi import FastAPI, HTTPException, status +from models_library.service_settings_labels import SimcoreServiceLabels from models_library.services import ServiceKey, ServiceVersion from models_library.services_resources import ServiceResourcesDict from models_library.users import UserID @@ -64,7 +65,7 @@ def create(cls, app: FastAPI, **kwargs): @classmethod def instance(cls, app: FastAPI) -> "CatalogClient": - assert type(app.state.catalog_client) == CatalogClient # nosec + assert isinstance(app.state.catalog_client, CatalogClient) # nosec return app.state.catalog_client @handle_errors("Catalog", logger) @@ -107,6 +108,18 @@ async def get_service_resources( return json_response raise HTTPException(status_code=resp.status_code, detail=resp.content) + async def get_service_labels( + self, service_key: ServiceKey, service_version: ServiceVersion + ) -> SimcoreServiceLabels: + resp = await self.request( + "GET", + f"/services/{urllib.parse.quote( service_key, safe='')}/{service_version}/labels", + ) + resp.raise_for_status() + if resp.status_code == status.HTTP_200_OK: + return SimcoreServiceLabels.model_validate(resp.json()) + raise HTTPException(status_code=resp.status_code, detail=resp.content) + async def get_service_specifications( self, user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion ) -> dict[str, Any]: diff --git a/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py b/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py index b703691926f..1a59ecd0e18 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py @@ -150,7 +150,7 @@ async def _get_node_infos( ] = await asyncio.gather( _get_service_details(catalog_client, user_id, product_name, node), director_client.get_service_extras(node.key, node.version), - director_client.get_service_labels(node), + catalog_client.get_service_labels(node.key, node.version), ) return result diff --git a/services/director-v2/src/simcore_service_director_v2/modules/director_v0.py b/services/director-v2/src/simcore_service_director_v2/modules/director_v0.py index ca211eb70dd..45b0b860e6c 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/director_v0.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/director_v0.py @@ -14,8 +14,7 @@ from models_library.api_schemas_directorv2.services import ServiceExtras from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID -from models_library.service_settings_labels import SimcoreServiceLabels -from models_library.services import ServiceKey, ServiceKeyVersion, ServiceVersion +from models_library.services import ServiceKey, ServiceVersion from models_library.users import UserID from servicelib.fastapi.tracing import setup_httpx_client_tracing from servicelib.logging_utils import log_decorator @@ -107,19 +106,6 @@ async def get_running_service_details( ) raise HTTPException(status_code=resp.status_code, detail=resp.content) - @log_decorator(logger=logger) - async def get_service_labels( - self, service: ServiceKeyVersion - ) -> SimcoreServiceLabels: - resp = await self._request( - "GET", - f"services/{urllib.parse.quote_plus(service.key)}/{service.version}/labels", - ) - resp.raise_for_status() - if resp.status_code == status.HTTP_200_OK: - return SimcoreServiceLabels.model_validate(unenvelope_or_raise_error(resp)) - raise HTTPException(status_code=resp.status_code, detail=resp.content) - @log_decorator(logger=logger) async def get_running_services( self, user_id: UserID | None = None, project_id: ProjectID | None = None diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/settings.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/settings.py index 8c1849064ee..dcefb810c2c 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/settings.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/settings.py @@ -14,7 +14,6 @@ SimcoreServiceSettingLabelEntry, SimcoreServiceSettingsLabel, ) -from models_library.services import ServiceKeyVersion from models_library.services_resources import ( CPU_100_PERCENT, DEFAULT_SINGLE_SERVICE_NAME, @@ -29,7 +28,7 @@ MATCH_SERVICE_VERSION, ) -from ....modules.director_v0 import DirectorV0Client +from ....modules.catalog import CatalogClient from ..errors import DynamicSidecarError BOOT_OPTION_PREFIX = "DY_BOOT_OPTION" @@ -193,7 +192,7 @@ def _assemble_key(service_key: str, service_tag: str) -> str: async def _extract_osparc_involved_service_labels( - director_v0_client: DirectorV0Client, + catalog_client: CatalogClient, service_key: str, service_tag: str, service_labels: SimcoreServiceLabels, @@ -252,10 +251,8 @@ def remap_to_compose_spec_key() -> dict[str, SimcoreServiceLabels]: reverse_mapping[involved_key] = compose_service_key simcore_service_labels: SimcoreServiceLabels = ( - await director_v0_client.get_service_labels( - service=ServiceKeyVersion( - key=current_service_key, version=current_service_tag - ) + await catalog_client.get_service_labels( + current_service_key, current_service_tag ) ) docker_image_name_by_services[involved_key] = simcore_service_labels @@ -432,14 +429,12 @@ def _assemble_env_vars_for_boot_options( async def get_labels_for_involved_services( - director_v0_client: DirectorV0Client, + catalog_client: CatalogClient, service_key: ServiceKey, service_tag: ServiceVersion, ) -> dict[str, SimcoreServiceLabels]: simcore_service_labels: SimcoreServiceLabels = ( - await director_v0_client.get_service_labels( - service=ServiceKeyVersion(key=service_key, version=service_tag) - ) + await catalog_client.get_service_labels(service_key, service_tag) ) log.info( "image=%s, tag=%s, labels=%s", service_key, service_tag, simcore_service_labels @@ -451,7 +446,7 @@ async def get_labels_for_involved_services( labels_for_involved_services: dict[ str, SimcoreServiceLabels ] = await _extract_osparc_involved_service_labels( - director_v0_client=director_v0_client, + catalog_client=catalog_client, service_key=service_key, service_tag=service_tag, service_labels=simcore_service_labels, @@ -461,7 +456,7 @@ async def get_labels_for_involved_services( async def merge_settings_before_use( - director_v0_client: DirectorV0Client, + catalog_client: CatalogClient, *, service_key: ServiceKey, service_tag: ServiceVersion, @@ -470,7 +465,7 @@ async def merge_settings_before_use( placement_substitutions: dict[str, DockerPlacementConstraint], ) -> SimcoreServiceSettingsLabel: labels_for_involved_services = await get_labels_for_involved_services( - director_v0_client=director_v0_client, + catalog_client=catalog_client, service_key=service_key, service_tag=service_tag, ) diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_event_create_sidecars.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_event_create_sidecars.py index 8352cab1f8a..6ea9efc4e37 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_event_create_sidecars.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_event_create_sidecars.py @@ -36,7 +36,6 @@ from ....catalog import CatalogClient from ....db.repositories.groups_extra_properties import GroupsExtraPropertiesRepository from ....db.repositories.projects import ProjectsRepository -from ....director_v0 import DirectorV0Client from ...docker_api import ( constrain_service_to_node, create_network, @@ -52,7 +51,7 @@ merge_settings_before_use, ) from ._abc import DynamicSchedulerEvent -from ._events_utils import get_allow_metrics_collection, get_director_v0_client +from ._events_utils import get_allow_metrics_collection _logger = logging.getLogger(__name__) @@ -171,7 +170,7 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None: # resources and placement derived from all the images in # the provided docker-compose spec # also other encodes the env vars to target the proper container - director_v0_client: DirectorV0Client = get_director_v0_client(app) + # fetching project form DB and fetching user settings projects_repository = get_repository(app, ProjectsRepository) @@ -188,8 +187,10 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None: ) _logger.info("%s", f"{boot_options=}") + catalog_client = CatalogClient.instance(app) + settings: SimcoreServiceSettingsLabel = await merge_settings_before_use( - director_v0_client=director_v0_client, + catalog_client=catalog_client, service_key=scheduler_data.key, service_tag=scheduler_data.version, service_user_selection_boot_options=boot_options, @@ -257,7 +258,6 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None: rpc_client=rpc_client, ) - catalog_client = CatalogClient.instance(app) user_specific_service_spec = ( await catalog_client.get_service_specifications( scheduler_data.user_id, scheduler_data.key, scheduler_data.version diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py index 289cfc162c9..721e5ee7af0 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py @@ -5,7 +5,7 @@ from models_library.projects import ProjectAtDB from models_library.projects_nodes_io import NodeIDStr from models_library.service_settings_labels import SimcoreServiceLabels -from models_library.services import ServiceKeyVersion, ServiceVersion +from models_library.services import ServiceVersion from models_library.services_creation import CreateServiceMetricsAdditionalParams from pydantic import TypeAdapter from servicelib.fastapi.long_running_tasks.client import TaskId @@ -19,16 +19,15 @@ DynamicServicesSchedulerSettings, ) from .....models.dynamic_services_scheduler import SchedulerData +from .....modules.catalog import CatalogClient from .....modules.instrumentation import get_instrumentation, get_metrics_labels from .....utils.db import get_repository from ....db.repositories.groups_extra_properties import GroupsExtraPropertiesRepository from ....db.repositories.projects import ProjectsRepository from ....db.repositories.users import UsersRepository -from ....director_v0 import DirectorV0Client from ...api_client import get_sidecars_client from ...docker_compose_specs import assemble_spec from ...errors import EntrypointContainerNotFoundError -from ._events_utils import get_director_v0_client _logger = logging.getLogger(__name__) @@ -63,12 +62,10 @@ async def submit_compose_sepc(app: FastAPI, scheduler_data: SchedulerData) -> No # creates a docker compose spec given the service key and tag # fetching project form DB and fetching user settings - director_v0_client: DirectorV0Client = get_director_v0_client(app) + catalog_client = CatalogClient.instance(app) simcore_service_labels: SimcoreServiceLabels = ( - await director_v0_client.get_service_labels( - service=ServiceKeyVersion( - key=scheduler_data.key, version=scheduler_data.version - ) + await catalog_client.get_service_labels( + scheduler_data.key, scheduler_data.version ) ) diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_utils.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_utils.py index 4a127e59e51..a516df060b1 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_utils.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_utils.py @@ -12,7 +12,6 @@ from models_library.projects_nodes_io import NodeID, NodeIDStr from models_library.rabbitmq_messages import InstrumentationRabbitMessage from models_library.service_settings_labels import SimcoreServiceLabels -from models_library.services import ServiceKeyVersion from models_library.shared_user_preferences import ( AllowMetricsCollectionFrontendUserPreference, ) @@ -52,6 +51,7 @@ DockerStatus, SchedulerData, ) +from .....modules.catalog import CatalogClient from .....modules.instrumentation import ( get_instrumentation, get_metrics_labels, @@ -534,12 +534,10 @@ async def _restore_service_state_with_metrics() -> None: await limited_gather(*tasks, limit=3) # inside this directory create the missing dirs, fetch those form the labels - director_v0_client: DirectorV0Client = get_director_v0_client(app) + catalog_client = CatalogClient.instance(app) simcore_service_labels: SimcoreServiceLabels = ( - await director_v0_client.get_service_labels( - service=ServiceKeyVersion( - key=scheduler_data.key, version=scheduler_data.version - ) + await catalog_client.get_service_labels( + scheduler_data.key, scheduler_data.version ) ) service_outputs_labels = json.loads( diff --git a/services/director-v2/src/simcore_service_director_v2/modules/projects_networks.py b/services/director-v2/src/simcore_service_director_v2/modules/projects_networks.py index e18dfc24121..f58a5ddcfbf 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/projects_networks.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/projects_networks.py @@ -22,9 +22,9 @@ from servicelib.utils import logged_gather from ..core.errors import ProjectNetworkNotFoundError +from ..modules.catalog import CatalogClient from ..modules.db.repositories.projects import ProjectsRepository from ..modules.db.repositories.projects_networks import ProjectsNetworksRepository -from ..modules.director_v0 import DirectorV0Client from ..modules.dynamic_sidecar.scheduler import DynamicSidecarsScheduler logger = logging.getLogger(__name__) @@ -51,7 +51,7 @@ def _network_name(project_id: ProjectID, user_defined: str) -> DockerNetworkName async def requires_dynamic_sidecar( service_key: str, service_version: str, - director_v0_client: DirectorV0Client, + catalog_client: CatalogClient, ) -> bool: decoded_service_key = urllib.parse.unquote_plus(service_key) @@ -62,11 +62,12 @@ async def requires_dynamic_sidecar( if service_type != "dynamic": return False + service_key_version = ServiceKeyVersion.model_validate( + {"key": decoded_service_key, "version": service_version} + ) simcore_service_labels: SimcoreServiceLabels = ( - await director_v0_client.get_service_labels( - service=ServiceKeyVersion.model_validate( - {"key": decoded_service_key, "version": service_version} - ) + await catalog_client.get_service_labels( + service_key_version.key, service_key_version.version ) ) requires_dynamic_sidecar_: bool = simcore_service_labels.needs_dynamic_sidecar @@ -176,7 +177,7 @@ async def _get_networks_with_aliases_for_default_network( project_id: ProjectID, user_id: UserID, new_workbench: NodesDict, - director_v0_client: DirectorV0Client, + catalog_client: CatalogClient, rabbitmq_client: RabbitMQClient, ) -> NetworksWithAliases: """ @@ -184,7 +185,9 @@ async def _get_networks_with_aliases_for_default_network( be on the same network. Return an updated version of the projects_networks """ - new_networks_with_aliases: NetworksWithAliases = NetworksWithAliases.model_validate({}) + new_networks_with_aliases: NetworksWithAliases = NetworksWithAliases.model_validate( + {} + ) default_network = _network_name(project_id, "default") new_networks_with_aliases[default_network] = ContainerAliases.model_validate({}) @@ -194,13 +197,15 @@ async def _get_networks_with_aliases_for_default_network( if not await requires_dynamic_sidecar( service_key=node_content.key, service_version=node_content.version, - director_v0_client=director_v0_client, + catalog_client=catalog_client, ): continue # only add if network label is valid, otherwise it will be skipped try: - network_alias = TypeAdapter(DockerNetworkAlias).validate_python(node_content.label) + network_alias = TypeAdapter(DockerNetworkAlias).validate_python( + node_content.label + ) except ValidationError: message = LoggerRabbitMessage( user_id=user_id, @@ -233,7 +238,7 @@ async def update_from_workbench( projects_networks_repository: ProjectsNetworksRepository, projects_repository: ProjectsRepository, scheduler: DynamicSidecarsScheduler, - director_v0_client: DirectorV0Client, + catalog_client: CatalogClient, rabbitmq_client: RabbitMQClient, project_id: ProjectID, ) -> None: @@ -262,7 +267,7 @@ async def update_from_workbench( project_id=project_id, user_id=project.prj_owner, new_workbench=project.workbench, - director_v0_client=director_v0_client, + catalog_client=catalog_client, rabbitmq_client=rabbitmq_client, ) logger.debug("%s", f"{existing_networks_with_aliases=}") diff --git a/services/director-v2/tests/unit/test_modules_dynamic_sidecar_scheduler.py b/services/director-v2/tests/unit/test_modules_dynamic_sidecar_scheduler.py index 5ffbc0ef96a..77c1e033ef6 100644 --- a/services/director-v2/tests/unit/test_modules_dynamic_sidecar_scheduler.py +++ b/services/director-v2/tests/unit/test_modules_dynamic_sidecar_scheduler.py @@ -5,7 +5,6 @@ import logging import re -import urllib.parse from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator from contextlib import asynccontextmanager, contextmanager from typing import Final @@ -18,14 +17,12 @@ from models_library.api_schemas_directorv2.dynamic_services_service import ( RunningDynamicServiceDetails, ) -from models_library.service_settings_labels import SimcoreServiceLabels from models_library.services_enums import ServiceState from models_library.wallets import WalletID from pydantic import NonNegativeFloat from pytest_mock.plugin import MockerFixture from pytest_simcore.helpers.typing_env import EnvVarsDict from respx.router import MockRouter -from simcore_service_director_v2.core.settings import AppSettings from simcore_service_director_v2.models.dynamic_services_scheduler import ( DockerContainerInspect, DynamicSidecarStatus, @@ -149,24 +146,6 @@ def mock_env( monkeypatch.setenv("S3_BUCKET_NAME", faker.pystr()) -@pytest.fixture -def mocked_director_v0( - minimal_config: AppSettings, scheduler_data: SchedulerData -) -> Iterator[MockRouter]: - endpoint = minimal_config.DIRECTOR_V0.endpoint - - with respx.mock as mock: - mock.get( - re.compile( - rf"^{endpoint}/services/{urllib.parse.quote_plus(scheduler_data.key)}/{scheduler_data.version}/labels" - ), - name="service labels", - ).respond( - json={"data": SimcoreServiceLabels.model_json_schema()["examples"][0]} - ) - yield mock - - @pytest.fixture def mocked_dynamic_scheduler_events(mocker: MockerFixture) -> None: class AlwaysTriggersDynamicSchedulerEvent(DynamicSchedulerEvent): diff --git a/services/director-v2/tests/unit/test_modules_project_networks.py b/services/director-v2/tests/unit/test_modules_project_networks.py index 848b3629e10..2c233ad4297 100644 --- a/services/director-v2/tests/unit/test_modules_project_networks.py +++ b/services/director-v2/tests/unit/test_modules_project_networks.py @@ -3,7 +3,7 @@ import json from pathlib import Path -from typing import Any, Iterable +from typing import Any from unittest.mock import AsyncMock, call from uuid import UUID, uuid4 @@ -159,7 +159,7 @@ def mock_scheduler() -> AsyncMock: @pytest.fixture -def mock_director_v0_client() -> AsyncMock: +def mock_catalog_client() -> AsyncMock: return AsyncMock() @@ -195,11 +195,11 @@ def fake_project_id() -> ProjectID: @pytest.fixture -def mock_docker_calls(mocker: MockerFixture) -> Iterable[dict[str, AsyncMock]]: +def mock_docker_calls(mocker: MockerFixture) -> dict[str, AsyncMock]: requires_dynamic_sidecar_mock = AsyncMock() requires_dynamic_sidecar_mock.return_value = True class_base = "simcore_service_director_v2.modules.dynamic_sidecar.scheduler._task.DynamicSidecarsScheduler" - mocked_items = { + return { "attach": mocker.patch(f"{class_base}.attach_project_network", AsyncMock()), "detach": mocker.patch(f"{class_base}.detach_project_network", AsyncMock()), "requires_dynamic_sidecar": mocker.patch( @@ -208,8 +208,6 @@ def mock_docker_calls(mocker: MockerFixture) -> Iterable[dict[str, AsyncMock]]: ), } - yield mocked_items - async def test_send_network_configuration_to_dynamic_sidecar( mock_scheduler: AsyncMock, @@ -230,7 +228,7 @@ async def test_send_network_configuration_to_dynamic_sidecar( async def test_get_networks_with_aliases_for_default_network_is_json_serializable( - mock_director_v0_client: AsyncMock, + mock_catalog_client: AsyncMock, fake_project_id: ProjectID, dy_workbench_with_networkable_labels: dict[str, Any], user_id: PositiveInt, @@ -241,6 +239,6 @@ async def test_get_networks_with_aliases_for_default_network_is_json_serializabl project_id=fake_project_id, user_id=user_id, new_workbench=dy_workbench_with_networkable_labels, - director_v0_client=mock_director_v0_client, + catalog_client=mock_catalog_client, rabbitmq_client=rabbitmq_client, ) diff --git a/services/director-v2/tests/unit/with_dbs/comp_scheduler/test_api_route_computations.py b/services/director-v2/tests/unit/with_dbs/comp_scheduler/test_api_route_computations.py index 2d076c3fdf0..2178ce7bfcb 100644 --- a/services/director-v2/tests/unit/with_dbs/comp_scheduler/test_api_route_computations.py +++ b/services/director-v2/tests/unit/with_dbs/comp_scheduler/test_api_route_computations.py @@ -135,7 +135,6 @@ def mocked_director_service_fcts( minimal_app: FastAPI, fake_service_details: ServiceMetaDataPublished, fake_service_extras: ServiceExtras, - fake_service_labels: dict[str, Any], ) -> Iterator[respx.MockRouter]: # pylint: disable=not-context-manager with respx.mock( @@ -151,12 +150,6 @@ def mocked_director_service_fcts( ).respond( json={"data": [fake_service_details.model_dump(mode="json", by_alias=True)]} ) - respx_mock.get( - re.compile( - r"/services/simcore%2Fservices%2F(comp|dynamic|frontend)%2F[^/]+/\d+.\d+.\d+/labels" - ), - name="get_service_labels", - ).respond(json={"data": fake_service_labels}) respx_mock.get( re.compile( @@ -175,6 +168,7 @@ def mocked_catalog_service_fcts( minimal_app: FastAPI, fake_service_details: ServiceMetaDataPublished, fake_service_resources: ServiceResourcesDict, + fake_service_labels: dict[str, Any], ) -> Iterator[respx.MockRouter]: def _mocked_service_resources(request) -> httpx.Response: return httpx.Response( @@ -223,6 +217,12 @@ def _mocked_services_details( ), name="get_service_resources", ).mock(side_effect=_mocked_service_resources) + respx_mock.get( + re.compile( + r"/services/simcore%2Fservices%2F(comp|dynamic|frontend)%2F[^/]+/\d+.\d+.\d+/labels" + ), + name="get_service_labels", + ).respond(json=fake_service_labels) respx_mock.get( re.compile( r"services/(?Psimcore%2Fservices%2F(comp|dynamic|frontend)%2F[^/]+)/(?P[^\.]+.[^\.]+.[^/\?]+).*" @@ -238,7 +238,7 @@ def mocked_catalog_service_fcts_deprecated( minimal_app: FastAPI, fake_service_details: ServiceMetaDataPublished, fake_service_extras: ServiceExtras, -): +) -> Iterator[respx.MockRouter]: def _mocked_services_details( request, service_key: str, service_version: str ) -> httpx.Response: @@ -259,7 +259,7 @@ def _mocked_services_details( } data = { - **ServiceGet.model_config["json_schema_extra"]["examples"][0], + **ServiceGet.model_json_schema()["examples"][0], **data_published, **deprecated, } # type: ignore @@ -402,8 +402,8 @@ async def test_computation_create_validators( async def test_create_computation( minimal_configuration: None, - mocked_director_service_fcts, - mocked_catalog_service_fcts, + mocked_director_service_fcts: respx.MockRouter, + mocked_catalog_service_fcts: respx.MockRouter, product_name: str, fake_workbench_without_outputs: dict[str, Any], registered_user: Callable[..., dict[str, Any]], @@ -678,8 +678,8 @@ async def test_create_computation_with_wallet_with_no_clusters_keeper_raises_503 async def test_start_computation_without_product_fails( minimal_configuration: None, - mocked_director_service_fcts, - mocked_catalog_service_fcts, + mocked_director_service_fcts: respx.MockRouter, + mocked_catalog_service_fcts: respx.MockRouter, product_name: str, fake_workbench_without_outputs: dict[str, Any], registered_user: Callable[..., dict[str, Any]], @@ -702,8 +702,8 @@ async def test_start_computation_without_product_fails( async def test_start_computation( minimal_configuration: None, - mocked_director_service_fcts, - mocked_catalog_service_fcts, + mocked_director_service_fcts: respx.MockRouter, + mocked_catalog_service_fcts: respx.MockRouter, product_name: str, fake_workbench_without_outputs: dict[str, Any], registered_user: Callable[..., dict[str, Any]], @@ -734,8 +734,8 @@ async def test_start_computation( async def test_start_computation_with_project_node_resources_defined( minimal_configuration: None, - mocked_director_service_fcts, - mocked_catalog_service_fcts, + mocked_director_service_fcts: respx.MockRouter, + mocked_catalog_service_fcts: respx.MockRouter, product_name: str, fake_workbench_without_outputs: dict[str, Any], registered_user: Callable[..., dict[str, Any]], @@ -779,8 +779,8 @@ async def test_start_computation_with_project_node_resources_defined( async def test_start_computation_with_deprecated_services_raises_406( minimal_configuration: None, - mocked_director_service_fcts, - mocked_catalog_service_fcts_deprecated, + mocked_director_service_fcts: respx.MockRouter, + mocked_catalog_service_fcts_deprecated: respx.MockRouter, product_name: str, fake_workbench_without_outputs: dict[str, Any], fake_workbench_adjacency: dict[str, Any], diff --git a/services/director-v2/tests/unit/with_dbs/test_api_route_dynamic_services.py b/services/director-v2/tests/unit/with_dbs/test_api_route_dynamic_services.py index a44b2838431..fd1d43e25aa 100644 --- a/services/director-v2/tests/unit/with_dbs/test_api_route_dynamic_services.py +++ b/services/director-v2/tests/unit/with_dbs/test_api_route_dynamic_services.py @@ -163,17 +163,11 @@ async def mock_retrieve_features( ) as respx_mock: if is_legacy: service_details = RunningDynamicServiceDetails.model_validate( - RunningDynamicServiceDetails.model_config["json_schema_extra"][ - "examples" - ][0] + RunningDynamicServiceDetails.model_json_schema()["examples"][0] ) respx_mock.post( f"{service_details.legacy_service_url}/retrieve", name="retrieve" - ).respond( - json=RetrieveDataOutEnveloped.model_config["json_schema_extra"][ - "examples" - ][0] - ) + ).respond(json=RetrieveDataOutEnveloped.model_json_schema()["examples"][0]) yield respx_mock # no cleanup required @@ -216,12 +210,12 @@ async def _mocked_context_manger(*args, **kwargs) -> AsyncIterator[int]: @pytest.fixture -def mocked_director_v0_service_api( +def mocked_catalog_service_api( minimal_app: FastAPI, service: dict[str, Any], service_labels: dict[str, Any] ) -> Iterator[MockRouter]: # pylint: disable=not-context-manager with respx.mock( - base_url=minimal_app.state.settings.DIRECTOR_V0.endpoint, + base_url=minimal_app.state.settings.DIRECTOR_V2_CATALOG.api_base_url, assert_all_called=False, assert_all_mocked=True, ) as respx_mock: @@ -229,16 +223,27 @@ def mocked_director_v0_service_api( respx_mock.get( f"/services/{urllib.parse.quote_plus(service['key'])}/{service['version']}/labels", name="service labels", - ).respond(json={"data": service_labels}) + ).respond(json=service_labels) + + yield respx_mock + +@pytest.fixture +def mocked_director_v0_service_api( + minimal_app: FastAPI, service: dict[str, Any], service_labels: dict[str, Any] +) -> Iterator[MockRouter]: + # pylint: disable=not-context-manager + with respx.mock( + base_url=minimal_app.state.settings.DIRECTOR_V0.endpoint, + assert_all_called=False, + assert_all_mocked=True, + ) as respx_mock: respx_mock.get( f"/running_interactive_services/{service['node_uuid']}", name="running interactive service", ).respond( json={ - "data": RunningDynamicServiceDetails.model_config["json_schema_extra"][ - "examples" - ][0] + "data": RunningDynamicServiceDetails.model_json_schema()["examples"][0] } ) @@ -255,9 +260,7 @@ def get_stack_status(node_uuid: NodeID) -> RunningDynamicServiceDetails: raise DynamicSidecarNotFoundError(node_uuid=node_uuid) return RunningDynamicServiceDetails.model_validate( - RunningDynamicServiceDetails.model_config["json_schema_extra"]["examples"][ - 0 - ] + RunningDynamicServiceDetails.model_json_schema()["examples"][0] ) module_base = "simcore_service_director_v2.modules.dynamic_sidecar.scheduler" @@ -289,9 +292,7 @@ def remove_service(node_uuid: NodeID, *ars: Any, **kwargs: Any) -> None: [ pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][0], exp_status_code=status.HTTP_307_TEMPORARY_REDIRECT, is_legacy=True, @@ -300,9 +301,7 @@ def remove_service(node_uuid: NodeID, *ars: Any, **kwargs: Any) -> None: ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][1], exp_status_code=status.HTTP_201_CREATED, is_legacy=False, @@ -311,9 +310,7 @@ def remove_service(node_uuid: NodeID, *ars: Any, **kwargs: Any) -> None: ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][2], exp_status_code=status.HTTP_201_CREATED, is_legacy=False, @@ -325,6 +322,7 @@ def remove_service(node_uuid: NodeID, *ars: Any, **kwargs: Any) -> None: def test_create_dynamic_services( minimal_config: None, mocked_director_v0_service_api: MockRouter, + mocked_catalog_service_api: MockRouter, mocked_director_v2_scheduler: None, client: TestClient, dynamic_sidecar_headers: dict[str, str], @@ -367,9 +365,7 @@ def test_create_dynamic_services( [ pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][0], exp_status_code=status.HTTP_307_TEMPORARY_REDIRECT, is_legacy=True, @@ -378,9 +374,7 @@ def test_create_dynamic_services( ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][1], exp_status_code=status.HTTP_200_OK, is_legacy=False, @@ -389,9 +383,7 @@ def test_create_dynamic_services( ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][2], exp_status_code=status.HTTP_200_OK, is_legacy=False, @@ -402,6 +394,7 @@ def test_create_dynamic_services( ) def test_get_service_status( mocked_director_v0_service_api: MockRouter, + mocked_catalog_service_api: MockRouter, mocked_director_v2_scheduler: None, client: TestClient, service: dict[str, Any], @@ -431,9 +424,7 @@ def test_get_service_status( [ pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][0], exp_status_code=status.HTTP_307_TEMPORARY_REDIRECT, is_legacy=True, @@ -442,9 +433,7 @@ def test_get_service_status( ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][1], exp_status_code=status.HTTP_204_NO_CONTENT, is_legacy=False, @@ -453,9 +442,7 @@ def test_get_service_status( ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][2], exp_status_code=status.HTTP_204_NO_CONTENT, is_legacy=False, @@ -467,9 +454,10 @@ def test_get_service_status( @pytest.mark.parametrize( "can_save, exp_save_state", [(None, True), (True, True), (False, False)] ) -def test_delete_service( +def test_delete_service( # pylint:disable=too-many-arguments docker_swarm: None, mocked_director_v0_service_api: MockRouter, + mocked_catalog_service_api: MockRouter, mocked_director_v2_scheduler: None, mocked_service_awaits_manual_interventions: None, client: TestClient, @@ -509,9 +497,7 @@ def dynamic_sidecar_scheduler(minimal_app: FastAPI) -> DynamicSidecarsScheduler: [ pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][1], exp_status_code=status.HTTP_201_CREATED, is_legacy=False, @@ -522,6 +508,7 @@ def dynamic_sidecar_scheduler(minimal_app: FastAPI) -> DynamicSidecarsScheduler: def test_delete_service_waiting_for_manual_intervention( minimal_config: None, mocked_director_v0_service_api: MockRouter, + mocked_catalog_service_api: MockRouter, mocked_director_v2_scheduler: None, client: TestClient, dynamic_sidecar_headers: dict[str, str], @@ -558,9 +545,7 @@ def test_delete_service_waiting_for_manual_intervention( [ pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][0], exp_status_code=status.HTTP_200_OK, is_legacy=True, @@ -569,9 +554,7 @@ def test_delete_service_waiting_for_manual_intervention( ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][1], exp_status_code=status.HTTP_200_OK, is_legacy=False, @@ -580,9 +563,7 @@ def test_delete_service_waiting_for_manual_intervention( ), pytest.param( *ServiceParams( - service=DynamicServiceCreate.model_config["json_schema_extra"][ - "example" - ], + service=DynamicServiceCreate.model_json_schema()["example"], service_labels=SimcoreServiceLabels.model_json_schema()["examples"][2], exp_status_code=status.HTTP_200_OK, is_legacy=False, @@ -595,6 +576,7 @@ def test_retrieve( minimal_config: None, mock_retrieve_features: MockRouter | None, mocked_director_v0_service_api: MockRouter, + mocked_catalog_service_api: MockRouter, mocked_director_v2_scheduler: None, client: TestClient, service: dict[str, Any], @@ -607,8 +589,7 @@ def test_retrieve( response.status_code == exp_status_code ), f"expected status code {exp_status_code}, received {response.status_code}: {response.text}" assert ( - response.json() - == RetrieveDataOutEnveloped.model_config["json_schema_extra"]["examples"][0] + response.json() == RetrieveDataOutEnveloped.model_json_schema()["examples"][0] ) diff --git a/services/director-v2/tests/unit/with_dbs/test_cli.py b/services/director-v2/tests/unit/with_dbs/test_cli.py index 1892e2a5a38..91d8f8a773e 100644 --- a/services/director-v2/tests/unit/with_dbs/test_cli.py +++ b/services/director-v2/tests/unit/with_dbs/test_cli.py @@ -7,6 +7,7 @@ from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable from contextlib import asynccontextmanager from typing import Any +from unittest.mock import AsyncMock import pytest import respx @@ -89,7 +90,7 @@ def mock_save_service_state(mocker: MockerFixture) -> None: def mock_save_service_state_as_failing(mocker: MockerFixture) -> None: async def _always_raise(*args, **kwargs) -> None: msg = "I AM FAILING NOW" - raise Exception(msg) # pylint: disable=broad-exception-raised + raise Exception(msg) # pylint: disable=broad-exception-raised # noqa: TRY002 mocker.patch( "simcore_service_director_v2.modules.dynamic_sidecar.api_client._public.SidecarsClient.save_service_state", @@ -107,9 +108,7 @@ def mock_get_node_state(mocker: MockerFixture) -> None: mocker.patch( "simcore_service_director_v2.cli._core._get_dy_service_state", return_value=DynamicServiceGet.model_validate( - RunningDynamicServiceDetails.model_config["json_schema_extra"]["examples"][ - 0 - ] + RunningDynamicServiceDetails.model_json_schema()["examples"][0] ), ) @@ -119,6 +118,14 @@ def task_id(faker: Faker) -> str: return f"tas_id.{faker.uuid4()}" +@pytest.fixture +def mock_catalog_instance(mocker: MockerFixture) -> None: + mocker.patch( + "simcore_service_director_v2.modules.catalog.CatalogClient.instance", + return_value=AsyncMock(), + ) + + @pytest.fixture async def mock_close_service_routes( mocker: MockerFixture, task_id: str @@ -181,6 +188,7 @@ def _format_cli_error(result: Result) -> str: def test_project_save_state_ok( + mock_catalog_instance: None, mock_requires_dynamic_sidecar: None, mock_save_service_state: None, cli_runner: CliRunner, @@ -206,6 +214,7 @@ def test_project_save_state_ok( def test_project_save_state_retry_3_times_and_fails( + mock_catalog_instance: None, mock_requires_dynamic_sidecar: None, mock_save_service_state_as_failing: None, cli_runner: CliRunner, diff --git a/services/director/src/simcore_service_director/api/rest/_services.py b/services/director/src/simcore_service_director/api/rest/_services.py index 157f5305d1b..a82699331ce 100644 --- a/services/director/src/simcore_service_director/api/rest/_services.py +++ b/services/director/src/simcore_service_director/api/rest/_services.py @@ -73,6 +73,7 @@ async def list_service_labels( service_key: ServiceKey, service_version: ServiceVersion, ) -> Envelope[dict[str, Any]]: + # NOTE: avoid using this directly via `director` service, call it via `catalog` service _logger.debug( "Retrieving service labels with service_key %s, service_version %s", service_key,