diff --git a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py index 5c170588856..3455e8a81ac 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/licensed_items.py @@ -3,7 +3,7 @@ from models_library.licensed_items import LicensedItemID, LicensedResourceType from models_library.resource_tracker import PricingPlanId -from pydantic import PositiveInt +from pydantic import ConfigDict, PositiveInt from ._base import OutputSchema @@ -15,6 +15,20 @@ class LicensedItemGet(OutputSchema): pricing_plan_id: PricingPlanId created_at: datetime modified_at: datetime + model_config = ConfigDict( + json_schema_extra={ + "examples": [ + { + "licensed_item_id": "0362b88b-91f8-4b41-867c-35544ad1f7a1", + "name": "best-model", + "licensed_resource_type": f"{LicensedResourceType.VIP_MODEL}", + "pricing_plan_id": "15", + "created_at": "2024-12-12 09:59:26.422140", + "modified_at": "2024-12-12 09:59:26.422140", + } + ] + } + ) class LicensedItemGetPage(NamedTuple): diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py b/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py index 16de774ce52..e90c60861eb 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/rabbitmq.py @@ -6,15 +6,23 @@ from servicelib.aiohttp.application_setup import ApplicationSetupError from servicelib.fastapi.dependencies import get_app from servicelib.rabbitmq import RabbitMQClient +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from tenacity import before_sleep_log, retry, stop_after_delay, wait_fixed -from ...services.log_streaming import LogDistributor +from ...services_http.log_streaming import LogDistributor _MAX_WAIT_FOR_LOG_DISTRIBUTOR_SECONDS: Final[int] = 10 _logger = logging.getLogger(__name__) +def get_rabbitmq_rpc_client( + app: Annotated[FastAPI, Depends(get_app)] +) -> RabbitMQRPCClient: + assert app.state.rabbitmq_rpc_client # nosec + return cast(RabbitMQRPCClient, app.state.rabbitmq_rpc_client) + + def get_rabbitmq_client(app: Annotated[FastAPI, Depends(get_app)]) -> RabbitMQClient: assert app.state.rabbitmq_client # nosec return cast(RabbitMQClient, app.state.rabbitmq_client) diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_http.py similarity index 98% rename from services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py rename to services/api-server/src/simcore_service_api_server/api/dependencies/webserver_http.py index 94cfed68b55..d70a64575e2 100644 --- a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_http.py @@ -9,7 +9,7 @@ from ..._constants import MSG_BACKEND_SERVICE_UNAVAILABLE from ...core.settings import ApplicationSettings, WebServerSettings -from ...services.webserver import AuthSession +from ...services_http.webserver import AuthSession from .application import get_app, get_settings from .authentication import Identity, get_active_user_email, get_current_identity diff --git a/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py new file mode 100644 index 00000000000..df47e0a5be9 --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/api/dependencies/webserver_rpc.py @@ -0,0 +1,13 @@ +from typing import Annotated, cast + +from fastapi import Depends, FastAPI +from servicelib.fastapi.dependencies import get_app + +from ...services_rpc.wb_api_server import WbApiRpcClient + + +async def get_wb_api_rpc_client( + app: Annotated[FastAPI, Depends(get_app)] +) -> WbApiRpcClient: + assert app.state.wb_api_rpc_client # nosec + return cast(WbApiRpcClient, app.state.wb_api_rpc_client) diff --git a/services/api-server/src/simcore_service_api_server/api/root.py b/services/api-server/src/simcore_service_api_server/api/root.py index aa715ade554..d73247b7baf 100644 --- a/services/api-server/src/simcore_service_api_server/api/root.py +++ b/services/api-server/src/simcore_service_api_server/api/root.py @@ -7,6 +7,7 @@ from .routes import ( files, health, + licensed_items, meta, solvers, solvers_jobs, @@ -40,6 +41,9 @@ def create_router(settings: ApplicationSettings): router.include_router(studies_jobs.router, tags=["studies"], prefix="/studies") router.include_router(wallets.router, tags=["wallets"], prefix="/wallets") router.include_router(_credits.router, tags=["credits"], prefix="/credits") + router.include_router( + licensed_items.router, tags=["licensed-items"], prefix="/licensed-items" + ) # NOTE: multiple-files upload is currently disabled # Web form to upload files at http://localhost:8000/v0/upload-form-view diff --git a/services/api-server/src/simcore_service_api_server/api/routes/credits.py b/services/api-server/src/simcore_service_api_server/api/routes/credits.py index 5b5258cfb01..36069b54988 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/credits.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/credits.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, status from ...models.schemas.model_adapter import GetCreditPriceLegacy -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION router = APIRouter() diff --git a/services/api-server/src/simcore_service_api_server/api/routes/files.py b/services/api-server/src/simcore_service_api_server/api/routes/files.py index 219306f693b..2187e68ad06 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/files.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/files.py @@ -37,7 +37,7 @@ FileUploadData, UploadLinks, ) -from ...services.storage import StorageApi, StorageFileMetaData, to_file_api_model +from ...services_http.storage import StorageApi, StorageFileMetaData, to_file_api_model from ..dependencies.authentication import get_current_user_id from ..dependencies.services import get_api_client from ._common import API_SERVER_DEV_FEATURES_ENABLED diff --git a/services/api-server/src/simcore_service_api_server/api/routes/health.py b/services/api-server/src/simcore_service_api_server/api/routes/health.py index 1537d1a5d65..0b7bfbcbc9f 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/health.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/health.py @@ -10,10 +10,10 @@ from ..._meta import API_VERSION, PROJECT_NAME from ...core.health_checker import ApiServerHealthChecker, get_health_checker -from ...services.catalog import CatalogApi -from ...services.director_v2 import DirectorV2Api -from ...services.storage import StorageApi -from ...services.webserver import WebserverApi +from ...services_http.catalog import CatalogApi +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.storage import StorageApi +from ...services_http.webserver import WebserverApi from ..dependencies.application import get_reverse_url_mapper from ..dependencies.services import get_api_client diff --git a/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py new file mode 100644 index 00000000000..d02151080ad --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/api/routes/licensed_items.py @@ -0,0 +1,34 @@ +from typing import Annotated, Any + +from fastapi import APIRouter, Depends, status + +from ...api.dependencies.authentication import get_product_name +from ...api.dependencies.webserver_rpc import get_wb_api_rpc_client +from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES +from ...models.pagination import Page, PaginationParams +from ...models.schemas.model_adapter import LicensedItemGet +from ...services_rpc.wb_api_server import WbApiRpcClient + +router = APIRouter() + +_LICENSE_ITEMS_STATUS_CODES: dict[int | str, dict[str, Any]] = { + **DEFAULT_BACKEND_SERVICE_STATUS_CODES, +} + + +@router.get( + "", + response_model=Page[LicensedItemGet], + status_code=status.HTTP_200_OK, + responses=_LICENSE_ITEMS_STATUS_CODES, + description="Get all licensed items", + include_in_schema=False, +) +async def get_licensed_items( + page_params: Annotated[PaginationParams, Depends()], + web_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + product_name: Annotated[str, Depends(get_product_name)], +): + return await web_api_rpc.get_licensed_items( + product_name=product_name, page_params=page_params + ) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py index c85b8b39baf..f9f80b37c4c 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers.py @@ -13,11 +13,11 @@ from ...models.schemas.errors import ErrorGet from ...models.schemas.model_adapter import ServicePricingPlanGetLegacy from ...models.schemas.solvers import Solver, SolverKeyId, SolverPort -from ...services.catalog import CatalogApi +from ...services_http.catalog import CatalogApi from ..dependencies.application import get_reverse_url_mapper from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.services import get_api_client -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._common import API_SERVER_DEV_FEATURES_ENABLED from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py index af1c80c70ac..609bf0b56a6 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py @@ -26,10 +26,10 @@ JobStatus, ) from ...models.schemas.solvers import Solver, SolverKeyId -from ...services.catalog import CatalogApi -from ...services.director_v2 import DirectorV2Api -from ...services.jobs import replace_custom_metadata, start_project, stop_project -from ...services.solver_job_models_converters import ( +from ...services_http.catalog import CatalogApi +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.jobs import replace_custom_metadata, start_project, stop_project +from ...services_http.solver_job_models_converters import ( create_job_from_project, create_jobstatus_from_task, create_new_project_for_job, @@ -37,7 +37,7 @@ from ..dependencies.application import get_reverse_url_mapper from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.services import get_api_client -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import ( FMSG_CHANGELOG_ADDED_IN_VERSION, FMSG_CHANGELOG_CHANGED_IN_VERSION, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py index 4903d1cb815..3c2bd479b0a 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs_getters.py @@ -38,22 +38,22 @@ WalletGetWithAvailableCreditsLegacy, ) from ...models.schemas.solvers import SolverKeyId -from ...services.catalog import CatalogApi -from ...services.director_v2 import DirectorV2Api -from ...services.jobs import ( +from ...services_http.catalog import CatalogApi +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.jobs import ( get_custom_metadata, raise_if_job_not_associated_with_solver, ) -from ...services.log_streaming import LogDistributor, LogStreamer -from ...services.solver_job_models_converters import create_job_from_project -from ...services.solver_job_outputs import ResultsTypes, get_solver_output_results -from ...services.storage import StorageApi, to_file_api_model +from ...services_http.log_streaming import LogDistributor, LogStreamer +from ...services_http.solver_job_models_converters import create_job_from_project +from ...services_http.solver_job_outputs import ResultsTypes, get_solver_output_results +from ...services_http.storage import StorageApi, to_file_api_model from ..dependencies.application import get_reverse_url_mapper from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.database import Engine, get_db_engine from ..dependencies.rabbitmq import get_log_check_timeout, get_log_distributor from ..dependencies.services import get_api_client -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION from .solvers_jobs import ( JOBS_STATUS_CODES, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies.py b/services/api-server/src/simcore_service_api_server/api/routes/studies.py index 392acd8c72a..d5f3e2c821e 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies.py @@ -6,15 +6,13 @@ from models_library.api_schemas_webserver.projects import ProjectGet from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID -from simcore_service_api_server.api.routes._constants import ( - FMSG_CHANGELOG_NEW_IN_VERSION, -) +from ...api.routes._constants import FMSG_CHANGELOG_NEW_IN_VERSION from ...models.pagination import OnePage, Page, PaginationParams from ...models.schemas.errors import ErrorGet from ...models.schemas.studies import Study, StudyID, StudyPort -from ...services.webserver import AuthSession -from ..dependencies.webserver import get_webserver_session +from ...services_http.webserver import AuthSession +from ..dependencies.webserver_http import get_webserver_session _logger = logging.getLogger(__name__) router = APIRouter() diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py index 8d23def5c0b..f1e5513414b 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py @@ -18,7 +18,6 @@ from ...api.dependencies.authentication import get_current_user_id from ...api.dependencies.services import get_api_client -from ...api.dependencies.webserver import get_webserver_session from ...exceptions.backend_errors import ProjectAlreadyStartedError from ...models.pagination import Page, PaginationParams from ...models.schemas.errors import ErrorGet @@ -32,22 +31,23 @@ JobStatus, ) from ...models.schemas.studies import JobLogsMap, Study, StudyID -from ...services.director_v2 import DirectorV2Api -from ...services.jobs import ( +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.jobs import ( get_custom_metadata, replace_custom_metadata, start_project, stop_project, ) -from ...services.solver_job_models_converters import create_jobstatus_from_task -from ...services.storage import StorageApi -from ...services.study_job_models_converters import ( +from ...services_http.solver_job_models_converters import create_jobstatus_from_task +from ...services_http.storage import StorageApi +from ...services_http.study_job_models_converters import ( create_job_from_study, create_job_outputs_from_project_outputs, get_project_and_file_inputs_from_job_inputs, ) -from ...services.webserver import AuthSession +from ...services_http.webserver import AuthSession from ..dependencies.application import get_reverse_url_mapper +from ..dependencies.webserver_http import get_webserver_session from ._common import API_SERVER_DEV_FEATURES_ENABLED from ._constants import FMSG_CHANGELOG_CHANGED_IN_VERSION, FMSG_CHANGELOG_NEW_IN_VERSION from .solvers_jobs import JOBS_STATUS_CODES diff --git a/services/api-server/src/simcore_service_api_server/api/routes/users.py b/services/api-server/src/simcore_service_api_server/api/routes/users.py index b63ff52f18c..1aee57c4648 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/users.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/users.py @@ -6,8 +6,8 @@ from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES from ...models.schemas.errors import ErrorGet from ...models.schemas.profiles import Profile, ProfileUpdate -from ...services.webserver import AuthSession -from ..dependencies.webserver import get_webserver_session +from ...services_http.webserver import AuthSession +from ..dependencies.webserver_http import get_webserver_session _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/wallets.py b/services/api-server/src/simcore_service_api_server/api/routes/wallets.py index 40e2233fce0..ca34726eae3 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/wallets.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/wallets.py @@ -6,7 +6,7 @@ from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES from ...models.schemas.errors import ErrorGet from ...models.schemas.model_adapter import WalletGetWithAvailableCreditsLegacy -from ..dependencies.webserver import AuthSession, get_webserver_session +from ..dependencies.webserver_http import AuthSession, get_webserver_session from ._constants import FMSG_CHANGELOG_NEW_IN_VERSION _logger = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/core/application.py b/services/api-server/src/simcore_service_api_server/core/application.py index 714d7993b6b..4de017698a6 100644 --- a/services/api-server/src/simcore_service_api_server/core/application.py +++ b/services/api-server/src/simcore_service_api_server/core/application.py @@ -12,8 +12,8 @@ from .._meta import API_VERSION, API_VTAG, APP_NAME from ..api.root import create_router from ..api.routes.health import router as health_router -from ..services import catalog, director_v2, storage, webserver -from ..services.rabbitmq import setup_rabbitmq +from ..services_http import catalog, director_v2, storage, webserver +from ..services_http.rabbitmq import setup_rabbitmq from ._prometheus_instrumentation import setup_prometheus_instrumentation from .events import create_start_app_handler, create_stop_app_handler from .openapi import override_openapi_method, use_route_names_as_operation_ids diff --git a/services/api-server/src/simcore_service_api_server/core/health_checker.py b/services/api-server/src/simcore_service_api_server/core/health_checker.py index 068b2d79f37..a05e046b2e8 100644 --- a/services/api-server/src/simcore_service_api_server/core/health_checker.py +++ b/services/api-server/src/simcore_service_api_server/core/health_checker.py @@ -16,7 +16,7 @@ from .._meta import PROJECT_NAME from ..models.schemas.jobs import JobID, JobLog -from ..services.log_streaming import LogDistributor +from ..services_http.log_streaming import LogDistributor METRICS_NAMESPACE: Final[str] = PROJECT_NAME.replace("-", "_") diff --git a/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py b/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py index d2779338980..91c4e0d9ccf 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py @@ -1,11 +1,11 @@ from fastapi import FastAPI from fastapi.exceptions import RequestValidationError from httpx import HTTPError as HttpxException -from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError from starlette import status from starlette.exceptions import HTTPException from ..._constants import MSG_INTERNAL_ERROR_USER_FRIENDLY_TEMPLATE +from ...exceptions.backend_errors import BaseBackEndError from ..custom_errors import CustomBaseError from ..log_streaming_errors import LogStreamingBaseError from ._custom_errors import custom_error_handler diff --git a/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py index e46d0f8f977..ca335deceac 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handlers_backend_errors.py @@ -1,7 +1,7 @@ -from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError from starlette.requests import Request from starlette.responses import JSONResponse +from ...exceptions.backend_errors import BaseBackEndError from ._utils import create_error_json_response diff --git a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py index 39167c92040..90bdc27bad4 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/service_errors_utils.py @@ -1,3 +1,5 @@ +# pylint: disable=dangerous-default-value +import asyncio import logging from collections.abc import Callable, Coroutine, Mapping from contextlib import contextmanager @@ -8,8 +10,9 @@ import httpx from fastapi import HTTPException, status from pydantic import ValidationError -from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError +from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError +from ..exceptions.backend_errors import BaseBackEndError from ..models.schemas.errors import ErrorGet _logger = logging.getLogger(__name__) @@ -50,8 +53,12 @@ class ToApiTuple(NamedTuple): # service to public-api status maps -E = TypeVar("E", bound=BaseBackEndError) -HttpStatusMap: TypeAlias = Mapping[ServiceHTTPStatus, E] +BackEndErrorType = TypeVar("BackEndErrorType", bound=BaseBackEndError) +RpcExceptionType = TypeVar( + "RpcExceptionType", bound=Exception +) # need more specific rpc exception base class +HttpStatusMap: TypeAlias = Mapping[ServiceHTTPStatus, BackEndErrorType] +RabbitMqRpcExceptionMap: TypeAlias = Mapping[RpcExceptionType, BackEndErrorType] def _get_http_exception_kwargs( @@ -98,6 +105,7 @@ def _get_http_exception_kwargs( def service_exception_handler( service_name: str, http_status_map: HttpStatusMap, + rpc_exception_map: RabbitMqRpcExceptionMap, **context, ): status_code: int @@ -126,20 +134,50 @@ def service_exception_handler( status_code=status_code, detail=detail, headers=headers ) from exc + except BaseException as exc: # currently no baseclass for rpc errors + if ( + type(exc) == asyncio.TimeoutError + ): # https://github.com/ITISFoundation/osparc-simcore/blob/master/packages/service-library/src/servicelib/rabbitmq/_client_rpc.py#L76 + raise HTTPException( + status_code=status.HTTP_504_GATEWAY_TIMEOUT, + detail="Request to backend timed out", + ) from exc + if type(exc) in { + asyncio.exceptions.CancelledError, + RuntimeError, + RemoteMethodNotRegisteredError, + }: # https://github.com/ITISFoundation/osparc-simcore/blob/master/packages/service-library/src/servicelib/rabbitmq/_client_rpc.py#L76 + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail="Request to backend failed", + ) from exc + if backend_error_type := rpc_exception_map.get(type(exc)): + raise backend_error_type(**context) from exc + raise + def service_exception_mapper( + *, service_name: str, - http_status_map: HttpStatusMap, + http_status_map: HttpStatusMap = {}, + rpc_exception_map: RabbitMqRpcExceptionMap = {}, ) -> Callable[ [Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]], Callable[Concatenate[Self, P], Coroutine[Any, Any, R]], ]: def _decorator(member_func: Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]): - _assert_correct_kwargs(func=member_func, status_map=http_status_map) + _assert_correct_kwargs( + func=member_func, + exception_types=set(http_status_map.values()).union( + set(rpc_exception_map.values()) + ), + ) @wraps(member_func) async def _wrapper(self: Self, *args: P.args, **kwargs: P.kwargs) -> R: - with service_exception_handler(service_name, http_status_map, **kwargs): + with service_exception_handler( + service_name, http_status_map, rpc_exception_map, **kwargs + ): return await member_func(self, *args, **kwargs) return _wrapper @@ -147,13 +185,14 @@ async def _wrapper(self: Self, *args: P.args, **kwargs: P.kwargs) -> R: return _decorator -def _assert_correct_kwargs(func: Callable, status_map: HttpStatusMap): +def _assert_correct_kwargs(func: Callable, exception_types: set[BackEndErrorType]): _required_kwargs = { name for name, param in signature(func).parameters.items() if param.kind == param.KEYWORD_ONLY } - for exc_type in status_map.values(): + for exc_type in exception_types: + assert isinstance(exc_type, type) # nosec _exception_inputs = exc_type.named_fields() assert _exception_inputs.issubset( _required_kwargs diff --git a/services/api-server/src/simcore_service_api_server/main.py b/services/api-server/src/simcore_service_api_server/main.py index 8b636ac4315..493874ee6eb 100644 --- a/services/api-server/src/simcore_service_api_server/main.py +++ b/services/api-server/src/simcore_service_api_server/main.py @@ -1,7 +1,8 @@ """Main application to be deployed in for example uvicorn. """ from fastapi import FastAPI -from simcore_service_api_server.core.application import init_app + +from .core.application import init_app # SINGLETON FastAPI app the_app: FastAPI = init_app() diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/meta.py b/services/api-server/src/simcore_service_api_server/models/schemas/meta.py index 9e195214ec0..4251d0f2a77 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/meta.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/meta.py @@ -2,7 +2,8 @@ from models_library.api_schemas__common.meta import BaseMeta from pydantic import ConfigDict, HttpUrl -from simcore_service_api_server.models._utils_pydantic import UriSchema + +from ...models._utils_pydantic import UriSchema class Meta(BaseMeta): diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py index 3b88cd82ef7..e5a04e198c5 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/model_adapter.py @@ -7,6 +7,9 @@ from models_library.api_schemas_api_server.pricing_plans import ( ServicePricingPlanGet as _ServicePricingPlanGet, ) +from models_library.api_schemas_webserver.licensed_items import ( + LicensedItemGet as _LicensedItemGet, +) from models_library.api_schemas_webserver.product import ( GetCreditPrice as _GetCreditPrice, ) @@ -18,6 +21,7 @@ ) from models_library.basic_types import IDStr, NonNegativeDecimal from models_library.groups import GroupID +from models_library.licensed_items import LicensedItemID, LicensedResourceType from models_library.resource_tracker import ( PricingPlanClassification, PricingPlanId, @@ -36,9 +40,6 @@ class GetCreditPriceLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) product_name: str = Field(alias="productName") usd_per_credit: ( Annotated[ @@ -58,6 +59,9 @@ class GetCreditPriceLegacy(BaseModel): "Can be None if this product's price is UNDEFINED", alias="minPaymentAmountUsd", ) + model_config = ConfigDict( + populate_by_name=True, + ) assert set(GetCreditPriceLegacy.model_fields.keys()) == set( @@ -66,9 +70,6 @@ class GetCreditPriceLegacy(BaseModel): class PricingUnitGetLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) pricing_unit_id: PricingUnitId = Field(alias="pricingUnitId") unit_name: str = Field(alias="unitName") unit_extra_info: UnitExtraInfo = Field(alias="unitExtraInfo") @@ -76,6 +77,9 @@ class PricingUnitGetLegacy(BaseModel): Decimal, PlainSerializer(float, return_type=NonNegativeFloat, when_used="json") ] = Field(alias="currentCostPerUnit") default: bool + model_config = ConfigDict( + populate_by_name=True, + ) assert set(PricingUnitGetLegacy.model_fields.keys()) == set( @@ -84,9 +88,6 @@ class PricingUnitGetLegacy(BaseModel): class WalletGetWithAvailableCreditsLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) wallet_id: WalletID = Field(alias="walletId") name: IDStr description: str | None = None @@ -98,6 +99,9 @@ class WalletGetWithAvailableCreditsLegacy(BaseModel): available_credits: Annotated[ Decimal, PlainSerializer(float, return_type=NonNegativeFloat, when_used="json") ] = Field(alias="availableCredits") + model_config = ConfigDict( + populate_by_name=True, + ) assert set(WalletGetWithAvailableCreditsLegacy.model_fields.keys()) == set( @@ -106,9 +110,6 @@ class WalletGetWithAvailableCreditsLegacy(BaseModel): class ServicePricingPlanGetLegacy(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) pricing_plan_id: PricingPlanId = Field(alias="pricingPlanId") display_name: str = Field(alias="displayName") description: str @@ -116,8 +117,28 @@ class ServicePricingPlanGetLegacy(BaseModel): created_at: datetime = Field(alias="createdAt") pricing_plan_key: str = Field(alias="pricingPlanKey") pricing_units: list[PricingUnitGetLegacy] = Field(alias="pricingUnits") + model_config = ConfigDict( + populate_by_name=True, + ) assert set(ServicePricingPlanGetLegacy.model_fields.keys()) == set( _ServicePricingPlanGet.model_fields.keys() ) + + +class LicensedItemGet(BaseModel): + licensed_item_id: LicensedItemID + name: Annotated[str, Field(alias="display_name")] + licensed_resource_type: LicensedResourceType + pricing_plan_id: PricingPlanId + created_at: datetime + modified_at: datetime + model_config = ConfigDict( + populate_by_name=True, + ) + + +assert set(LicensedItemGet.model_fields.keys()) == set( + _LicensedItemGet.model_fields.keys() +) diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py b/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py index 9db1e9696ad..f3be20211cd 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/solvers.py @@ -7,8 +7,8 @@ from models_library.services_regex import COMPUTATIONAL_SERVICE_KEY_RE from packaging.version import Version from pydantic import BaseModel, ConfigDict, Field, HttpUrl, StringConstraints -from simcore_service_api_server.models._utils_pydantic import UriSchema +from ...models._utils_pydantic import UriSchema from ..api_resources import compose_resource_name from ..basic_types import VersionStr diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/studies.py b/services/api-server/src/simcore_service_api_server/models/schemas/studies.py index 1905477236f..e0686713508 100644 --- a/services/api-server/src/simcore_service_api_server/models/schemas/studies.py +++ b/services/api-server/src/simcore_service_api_server/models/schemas/studies.py @@ -2,8 +2,8 @@ from models_library import projects, projects_nodes_io from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field -from simcore_service_api_server.models._utils_pydantic import UriSchema +from ...models._utils_pydantic import UriSchema from .. import api_resources from . import solvers diff --git a/services/api-server/src/simcore_service_api_server/services/__init__.py b/services/api-server/src/simcore_service_api_server/services_http/__init__.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/__init__.py rename to services/api-server/src/simcore_service_api_server/services_http/__init__.py diff --git a/services/api-server/src/simcore_service_api_server/services/catalog.py b/services/api-server/src/simcore_service_api_server/services_http/catalog.py similarity index 94% rename from services/api-server/src/simcore_service_api_server/services/catalog.py rename to services/api-server/src/simcore_service_api_server/services_http/catalog.py index 34f092a6191..c380025c672 100644 --- a/services/api-server/src/simcore_service_api_server/services/catalog.py +++ b/services/api-server/src/simcore_service_api_server/services_http/catalog.py @@ -13,11 +13,11 @@ from pydantic import ConfigDict, TypeAdapter, ValidationError from settings_library.catalog import CatalogSettings from settings_library.tracing import TracingSettings -from simcore_service_api_server.exceptions.backend_errors import ( + +from ..exceptions.backend_errors import ( ListSolversOrStudiesError, SolverOrStudyNotFoundError, ) - from ..exceptions.service_errors_utils import service_exception_mapper from ..models.basic_types import VersionStr from ..models.schemas.solvers import LATEST_VERSION, Solver, SolverKeyId, SolverPort @@ -68,7 +68,7 @@ def to_solver(self) -> Solver: # - Error handling: What do we reraise, suppress, transform??? # -_exception_mapper = partial(service_exception_mapper, "Catalog") +_exception_mapper = partial(service_exception_mapper, service_name="Catalog") TruncatedCatalogServiceOutAdapter: Final[ TypeAdapter[TruncatedCatalogServiceOut] @@ -91,7 +91,9 @@ class CatalogApi(BaseServiceClientApi): SEE osparc-simcore/services/catalog/openapi.json """ - @_exception_mapper({status.HTTP_404_NOT_FOUND: ListSolversOrStudiesError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ListSolversOrStudiesError} + ) async def list_solvers( self, *, @@ -134,7 +136,9 @@ async def list_solvers( ) return solvers - @_exception_mapper({status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError} + ) async def get_service( self, *, user_id: int, name: SolverKeyId, version: VersionStr, product_name: str ) -> Solver: @@ -163,7 +167,9 @@ async def get_service( solver: Solver = service.to_solver() return solver - @_exception_mapper({status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: SolverOrStudyNotFoundError} + ) async def get_service_ports( self, *, user_id: int, name: SolverKeyId, version: VersionStr, product_name: str ): diff --git a/services/api-server/src/simcore_service_api_server/services/director_v2.py b/services/api-server/src/simcore_service_api_server/services_http/director_v2.py similarity index 91% rename from services/api-server/src/simcore_service_api_server/services/director_v2.py rename to services/api-server/src/simcore_service_api_server/services_http/director_v2.py index 45f42af73eb..5a40fae7ca4 100644 --- a/services/api-server/src/simcore_service_api_server/services/director_v2.py +++ b/services/api-server/src/simcore_service_api_server/services_http/director_v2.py @@ -60,11 +60,11 @@ class TaskLogFileGet(BaseModel): # API CLASS --------------------------------------------- -_exception_mapper = partial(service_exception_mapper, "Director V2") +_exception_mapper = partial(service_exception_mapper, service_name="Director V2") class DirectorV2Api(BaseServiceClientApi): - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def create_computation( self, *, @@ -85,7 +85,7 @@ async def create_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def start_computation( self, *, @@ -115,7 +115,7 @@ async def start_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({status.HTTP_404_NOT_FOUND: JobNotFoundError}) + @_exception_mapper(http_status_map={status.HTTP_404_NOT_FOUND: JobNotFoundError}) async def get_computation( self, *, project_id: UUID, user_id: PositiveInt ) -> ComputationTaskGet: @@ -129,7 +129,7 @@ async def get_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({status.HTTP_404_NOT_FOUND: JobNotFoundError}) + @_exception_mapper(http_status_map={status.HTTP_404_NOT_FOUND: JobNotFoundError}) async def stop_computation( self, *, project_id: UUID, user_id: PositiveInt ) -> ComputationTaskGet: @@ -143,7 +143,7 @@ async def stop_computation( task: ComputationTaskGet = ComputationTaskGet.model_validate_json(response.text) return task - @_exception_mapper({status.HTTP_404_NOT_FOUND: JobNotFoundError}) + @_exception_mapper(http_status_map={status.HTTP_404_NOT_FOUND: JobNotFoundError}) async def delete_computation(self, *, project_id: UUID, user_id: PositiveInt): response = await self.client.request( "DELETE", @@ -155,7 +155,9 @@ async def delete_computation(self, *, project_id: UUID, user_id: PositiveInt): ) response.raise_for_status() - @_exception_mapper({status.HTTP_404_NOT_FOUND: LogFileNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: LogFileNotFoundError} + ) async def get_computation_logs( self, *, user_id: PositiveInt, project_id: UUID ) -> JobLogsMap: diff --git a/services/api-server/src/simcore_service_api_server/services/jobs.py b/services/api-server/src/simcore_service_api_server/services_http/jobs.py similarity index 97% rename from services/api-server/src/simcore_service_api_server/services/jobs.py rename to services/api-server/src/simcore_service_api_server/services_http/jobs.py index 277f9625f17..ed2ef50d588 100644 --- a/services/api-server/src/simcore_service_api_server/services/jobs.py +++ b/services/api-server/src/simcore_service_api_server/services_http/jobs.py @@ -9,7 +9,7 @@ from ..api.dependencies.authentication import get_current_user_id from ..api.dependencies.services import get_api_client -from ..api.dependencies.webserver import get_webserver_session +from ..api.dependencies.webserver_http import get_webserver_session from ..models.schemas.jobs import ( JobID, JobMetadata, diff --git a/services/api-server/src/simcore_service_api_server/services/log_streaming.py b/services/api-server/src/simcore_service_api_server/services_http/log_streaming.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/log_streaming.py rename to services/api-server/src/simcore_service_api_server/services_http/log_streaming.py diff --git a/services/api-server/src/simcore_service_api_server/services/rabbitmq.py b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py similarity index 72% rename from services/api-server/src/simcore_service_api_server/services/rabbitmq.py rename to services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py index 7b72e54b5ea..07201402876 100644 --- a/services/api-server/src/simcore_service_api_server/services/rabbitmq.py +++ b/services/api-server/src/simcore_service_api_server/services_http/rabbitmq.py @@ -2,10 +2,13 @@ from fastapi import FastAPI from servicelib.rabbitmq import RabbitMQClient, wait_till_rabbitmq_responsive +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient from settings_library.rabbit import RabbitSettings -from simcore_service_api_server.core.health_checker import ApiServerHealthChecker +from simcore_service_api_server.api.dependencies.rabbitmq import get_rabbitmq_rpc_client +from simcore_service_api_server.services_rpc import wb_api_server -from ..services.log_streaming import LogDistributor +from ..core.health_checker import ApiServerHealthChecker +from ..services_http.log_streaming import LogDistributor _logger = logging.getLogger(__name__) @@ -18,6 +21,9 @@ def setup_rabbitmq(app: FastAPI) -> None: async def _on_startup() -> None: await wait_till_rabbitmq_responsive(settings.dsn) + app.state.rabbitmq_rpc_client = await RabbitMQRPCClient.create( + client_name="api_server", settings=settings + ) app.state.rabbitmq_client = RabbitMQClient( client_name="api_server", settings=settings ) @@ -32,6 +38,7 @@ async def _on_startup() -> None: await app.state.health_checker.setup( app.state.settings.API_SERVER_HEALTH_CHECK_TASK_PERIOD_SECONDS ) + wb_api_server.setup(app, get_rabbitmq_rpc_client(app)) async def _on_shutdown() -> None: if app.state.health_checker: @@ -40,6 +47,8 @@ async def _on_shutdown() -> None: await app.state.log_distributor.teardown() if app.state.rabbitmq_client: await app.state.rabbitmq_client.close() + if app.state.rabbitmq_rpc_client: + await app.state.rabbitmq_rpc_client.close() app.add_event_handler("startup", _on_startup) app.add_event_handler("shutdown", _on_shutdown) diff --git a/services/api-server/src/simcore_service_api_server/services/solver_job_models_converters.py b/services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/solver_job_models_converters.py rename to services/api-server/src/simcore_service_api_server/services_http/solver_job_models_converters.py diff --git a/services/api-server/src/simcore_service_api_server/services/solver_job_outputs.py b/services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py similarity index 94% rename from services/api-server/src/simcore_service_api_server/services/solver_job_outputs.py rename to services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py index dac7610b5a3..5457a259f8c 100644 --- a/services/api-server/src/simcore_service_api_server/services/solver_job_outputs.py +++ b/services/api-server/src/simcore_service_api_server/services_http/solver_job_outputs.py @@ -7,9 +7,8 @@ from pydantic import StrictBool, StrictFloat, StrictInt, TypeAdapter from simcore_sdk import node_ports_v2 from simcore_sdk.node_ports_v2 import DBManager, Nodeports -from simcore_service_api_server.exceptions.backend_errors import ( - SolverOutputNotFoundError, -) + +from ..exceptions.backend_errors import SolverOutputNotFoundError log = logging.getLogger(__name__) diff --git a/services/api-server/src/simcore_service_api_server/services/storage.py b/services/api-server/src/simcore_service_api_server/services_http/storage.py similarity index 95% rename from services/api-server/src/simcore_service_api_server/services/storage.py rename to services/api-server/src/simcore_service_api_server/services_http/storage.py index 0095dd343f5..aa8b724ce98 100644 --- a/services/api-server/src/simcore_service_api_server/services/storage.py +++ b/services/api-server/src/simcore_service_api_server/services_http/storage.py @@ -24,7 +24,7 @@ _logger = logging.getLogger(__name__) -_exception_mapper = partial(service_exception_mapper, "Storage") +_exception_mapper = partial(service_exception_mapper, service_name="Storage") _FILE_ID_PATTERN = re.compile(r"^api\/(?P[\w-]+)\/(?P.+)$") AccessRight = Literal["read", "write"] @@ -54,7 +54,7 @@ class StorageApi(BaseServiceClientApi): # SIMCORE_S3_ID = 0 - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def list_files( self, *, @@ -81,7 +81,7 @@ async def list_files( ) return files - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def search_owned_files( self, *, @@ -119,7 +119,7 @@ async def search_owned_files( assert len(files) <= limit if limit else True # nosec return files - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def get_download_link( self, *, user_id: int, file_id: UUID, file_name: str ) -> AnyUrl: @@ -138,7 +138,7 @@ async def get_download_link( link: AnyUrl = presigned_link.link return link - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def delete_file(self, *, user_id: int, quoted_storage_file_id: str) -> None: response = await self.client.delete( f"/locations/{self.SIMCORE_S3_ID}/files/{quoted_storage_file_id}", @@ -146,7 +146,7 @@ async def delete_file(self, *, user_id: int, quoted_storage_file_id: str) -> Non ) response.raise_for_status() - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def get_upload_links( self, *, user_id: int, file_id: UUID, file_name: str ) -> FileUploadSchema: @@ -183,7 +183,7 @@ async def create_abort_upload_link( url = url.include_query_params(**query) return url - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def create_soft_link( self, *, user_id: int, target_s3_path: str, as_file_id: UUID ) -> File: diff --git a/services/api-server/src/simcore_service_api_server/services/study_job_models_converters.py b/services/api-server/src/simcore_service_api_server/services_http/study_job_models_converters.py similarity index 100% rename from services/api-server/src/simcore_service_api_server/services/study_job_models_converters.py rename to services/api-server/src/simcore_service_api_server/services_http/study_job_models_converters.py diff --git a/services/api-server/src/simcore_service_api_server/services/webserver.py b/services/api-server/src/simcore_service_api_server/services_http/webserver.py similarity index 92% rename from services/api-server/src/simcore_service_api_server/services/webserver.py rename to services/api-server/src/simcore_service_api_server/services_http/webserver.py index c7d5680eb37..a66bd7ff3d3 100644 --- a/services/api-server/src/simcore_service_api_server/services/webserver.py +++ b/services/api-server/src/simcore_service_api_server/services_http/webserver.py @@ -46,9 +46,18 @@ X_SIMCORE_PARENT_PROJECT_UUID, ) from settings_library.tracing import TracingSettings -from simcore_service_api_server.exceptions.backend_errors import ( +from tenacity import TryAgain +from tenacity.asyncio import AsyncRetrying +from tenacity.before_sleep import before_sleep_log +from tenacity.stop import stop_after_delay +from tenacity.wait import wait_fixed + +from ..core.settings import WebServerSettings +from ..exceptions.backend_errors import ( + ClusterNotFoundError, ConfigurationError, ForbiddenWalletError, + JobNotFoundError, ListJobsError, PaymentRequiredError, PricingPlanNotFoundError, @@ -61,15 +70,6 @@ SolverOutputNotFoundError, WalletNotFoundError, ) -from simcore_service_api_server.models.schemas.model_adapter import GetCreditPriceLegacy -from tenacity import TryAgain -from tenacity.asyncio import AsyncRetrying -from tenacity.before_sleep import before_sleep_log -from tenacity.stop import stop_after_delay -from tenacity.wait import wait_fixed - -from ..core.settings import WebServerSettings -from ..exceptions.backend_errors import ClusterNotFoundError, JobNotFoundError from ..exceptions.service_errors_utils import ( service_exception_handler, service_exception_mapper, @@ -78,6 +78,7 @@ from ..models.pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE from ..models.schemas.jobs import MetaValueType from ..models.schemas.model_adapter import ( + GetCreditPriceLegacy, PricingUnitGetLegacy, WalletGetWithAvailableCreditsLegacy, ) @@ -88,7 +89,7 @@ _logger = logging.getLogger(__name__) -_exception_mapper = partial(service_exception_mapper, "Webserver") +_exception_mapper = partial(service_exception_mapper, service_name="Webserver") _JOB_STATUS_MAP = { status.HTTP_402_PAYMENT_REQUIRED: PaymentRequiredError, @@ -196,6 +197,7 @@ async def _page_projects( with service_exception_handler( service_name="Webserver", http_status_map={status.HTTP_404_NOT_FOUND: ListJobsError}, + rpc_exception_map={}, ): resp = await self.client.get( "/projects", @@ -243,7 +245,7 @@ async def _wait_for_long_running_task_results(self, lrt_response: httpx.Response # PROFILE -------------------------------------------------- - @_exception_mapper(_PROFILE_STATUS_MAP) + @_exception_mapper(http_status_map=_PROFILE_STATUS_MAP) async def get_me(self) -> Profile: response = await self.client.get("/me", cookies=self.session_cookies) response.raise_for_status() @@ -263,7 +265,7 @@ async def get_me(self) -> Profile: gravatar_id=got.gravatar_id, ) - @_exception_mapper(_PROFILE_STATUS_MAP) + @_exception_mapper(http_status_map=_PROFILE_STATUS_MAP) async def update_me(self, *, profile_update: ProfileUpdate) -> Profile: update = WebProfileUpdate.model_construct( @@ -283,7 +285,7 @@ async def update_me(self, *, profile_update: ProfileUpdate) -> Profile: # PROJECTS ------------------------------------------------- - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def create_project( self, project: ProjectCreateNew, @@ -308,7 +310,7 @@ async def create_project( result = await self._wait_for_long_running_task_results(response) return ProjectGet.model_validate(result) - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def clone_project( self, *, @@ -333,7 +335,7 @@ async def clone_project( result = await self._wait_for_long_running_task_results(response) return ProjectGet.model_validate(result) - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def get_project(self, *, project_id: UUID) -> ProjectGet: response = await self.client.get( f"/projects/{project_id}", @@ -363,7 +365,7 @@ async def get_projects_page(self, *, limit: int, offset: int): show_hidden=False, ) - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def delete_project(self, *, project_id: ProjectID) -> None: response = await self.client.delete( f"/projects/{project_id}", @@ -371,7 +373,9 @@ async def delete_project(self, *, project_id: ProjectID) -> None: ) response.raise_for_status() - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProjectPortsNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProjectPortsNotFoundError} + ) async def get_project_metadata_ports( self, *, project_id: ProjectID ) -> list[StudyPort]: @@ -389,7 +393,9 @@ async def get_project_metadata_ports( assert isinstance(data, list) # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError} + ) async def get_project_metadata( self, *, project_id: ProjectID ) -> ProjectMetadataGet: @@ -402,7 +408,7 @@ async def get_project_metadata( assert data is not None # nosec return data - @_exception_mapper(_JOB_STATUS_MAP) + @_exception_mapper(http_status_map=_JOB_STATUS_MAP) async def patch_project(self, *, project_id: UUID, patch_params: ProjectPatch): response = await self.client.patch( f"/projects/{project_id}", @@ -411,7 +417,9 @@ async def patch_project(self, *, project_id: UUID, patch_params: ProjectPatch): ) response.raise_for_status() - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProjectMetadataNotFoundError} + ) async def update_project_metadata( self, *, project_id: ProjectID, metadata: dict[str, MetaValueType] ) -> ProjectMetadataGet: @@ -425,7 +433,9 @@ async def update_project_metadata( assert data is not None # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError} + ) async def get_project_node_pricing_unit( self, *, project_id: UUID, node_id: UUID ) -> PricingUnitGetLegacy: @@ -439,7 +449,9 @@ async def get_project_node_pricing_unit( assert data is not None # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: PricingUnitNotFoundError} + ) async def connect_pricing_unit_to_project_node( self, *, @@ -455,7 +467,7 @@ async def connect_pricing_unit_to_project_node( response.raise_for_status() @_exception_mapper( - _JOB_STATUS_MAP + http_status_map=_JOB_STATUS_MAP | { status.HTTP_409_CONFLICT: ProjectAlreadyStartedError, status.HTTP_406_NOT_ACCEPTABLE: ClusterNotFoundError, @@ -477,7 +489,7 @@ async def start_project( ) response.raise_for_status() - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def update_project_inputs( self, *, @@ -498,7 +510,7 @@ async def update_project_inputs( assert data is not None # nosec return data - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def get_project_inputs( self, *, project_id: ProjectID ) -> dict[NodeID, ProjectInputGet]: @@ -517,7 +529,9 @@ async def get_project_inputs( assert data is not None # nosec return data - @_exception_mapper({status.HTTP_404_NOT_FOUND: SolverOutputNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: SolverOutputNotFoundError} + ) async def get_project_outputs( self, *, project_id: ProjectID ) -> dict[NodeID, dict[str, Any]]: @@ -536,7 +550,7 @@ async def get_project_outputs( assert data is not None # nosec return data - @_exception_mapper({}) + @_exception_mapper(http_status_map={}) async def update_node_outputs( self, *, project_id: UUID, node_id: UUID, new_node_outputs: NodeOutputs ) -> None: @@ -549,7 +563,7 @@ async def update_node_outputs( # WALLETS ------------------------------------------------- - @_exception_mapper(_WALLET_STATUS_MAP) + @_exception_mapper(http_status_map=_WALLET_STATUS_MAP) async def get_default_wallet(self) -> WalletGetWithAvailableCreditsLegacy: response = await self.client.get( "/wallets/default", @@ -564,7 +578,7 @@ async def get_default_wallet(self) -> WalletGetWithAvailableCreditsLegacy: assert data is not None # nosec return data - @_exception_mapper(_WALLET_STATUS_MAP) + @_exception_mapper(http_status_map=_WALLET_STATUS_MAP) async def get_wallet( self, *, wallet_id: int ) -> WalletGetWithAvailableCreditsLegacy: @@ -581,7 +595,7 @@ async def get_wallet( assert data is not None # nosec return data - @_exception_mapper(_WALLET_STATUS_MAP) + @_exception_mapper(http_status_map=_WALLET_STATUS_MAP) async def get_project_wallet(self, *, project_id: ProjectID) -> WalletGet: response = await self.client.get( f"/projects/{project_id}/wallet", @@ -594,7 +608,9 @@ async def get_project_wallet(self, *, project_id: ProjectID) -> WalletGet: # PRODUCTS ------------------------------------------------- - @_exception_mapper({status.HTTP_404_NOT_FOUND: ProductPriceNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: ProductPriceNotFoundError} + ) async def get_product_price(self) -> GetCreditPriceLegacy: response = await self.client.get( "/credits-price", @@ -607,7 +623,9 @@ async def get_product_price(self) -> GetCreditPriceLegacy: # SERVICES ------------------------------------------------- - @_exception_mapper({status.HTTP_404_NOT_FOUND: PricingPlanNotFoundError}) + @_exception_mapper( + http_status_map={status.HTTP_404_NOT_FOUND: PricingPlanNotFoundError} + ) async def get_service_pricing_plan( self, *, solver_key: SolverKeyId, version: VersionStr ) -> ServicePricingPlanGet | None: diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/__init__.py b/services/api-server/src/simcore_service_api_server/services_rpc/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py new file mode 100644 index 00000000000..207712e3e47 --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass +from functools import partial +from typing import cast + +from fastapi import FastAPI +from fastapi_pagination import Page, create_page +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( + get_licensed_items as _get_licensed_items, +) + +from ..exceptions.service_errors_utils import service_exception_mapper +from ..models.pagination import PaginationParams +from ..models.schemas.model_adapter import LicensedItemGet + +_exception_mapper = partial(service_exception_mapper, service_name="WebApiServer") + + +@dataclass +class WbApiRpcClient: + _client: RabbitMQRPCClient + + @_exception_mapper(rpc_exception_map={}) + async def get_licensed_items( + self, product_name: str, page_params: PaginationParams + ) -> Page[LicensedItemGet]: + licensed_items_page = await _get_licensed_items( + rabbitmq_rpc_client=self._client, + product_name=product_name, + offset=page_params.offset, + limit=page_params.limit, + ) + page = create_page( + [ + LicensedItemGet( + licensed_item_id=elm.licensed_item_id, + name=elm.name, + licensed_resource_type=elm.licensed_resource_type, + pricing_plan_id=elm.pricing_plan_id, + created_at=elm.created_at, + modified_at=elm.modified_at, + ) + for elm in licensed_items_page.items + ], + total=licensed_items_page.total, + params=page_params, + ) + return cast(Page[LicensedItemGet], page) + + +def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient): + app.state.wb_api_rpc_client = WbApiRpcClient(_client=rabbitmq_rmp_client) diff --git a/services/api-server/tests/unit/api_solvers/conftest.py b/services/api-server/tests/unit/api_solvers/conftest.py index ec8bf7d5630..4d008380aaf 100644 --- a/services/api-server/tests/unit/api_solvers/conftest.py +++ b/services/api-server/tests/unit/api_solvers/conftest.py @@ -16,7 +16,7 @@ from pytest_simcore.helpers import faker_catalog from respx import MockRouter from simcore_service_api_server.core.settings import ApplicationSettings -from simcore_service_api_server.services.director_v2 import ComputationTaskGet +from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet @pytest.fixture diff --git a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py index 865983537b0..e3f97def12d 100644 --- a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py +++ b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs.py @@ -22,7 +22,7 @@ from simcore_service_api_server._meta import API_VTAG from simcore_service_api_server.core.settings import ApplicationSettings from simcore_service_api_server.models.schemas.jobs import Job, JobInputs, JobStatus -from simcore_service_api_server.services.director_v2 import ComputationTaskGet +from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet from starlette import status diff --git a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py index eb821e46d01..80a7176ca85 100644 --- a/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py +++ b/services/api-server/tests/unit/api_solvers/test_api_routers_solvers_jobs_logs.py @@ -79,7 +79,7 @@ def fake_project_for_streaming( fake_project = ProjectGet.model_validate(data) fake_project.workbench = {faker.uuid4(): faker.uuid4()} mocker.patch( - "simcore_service_api_server.api.dependencies.webserver.AuthSession.get_project", + "simcore_service_api_server.api.dependencies.webserver_http.AuthSession.get_project", return_value=fake_project, ) diff --git a/services/api-server/tests/unit/conftest.py b/services/api-server/tests/unit/conftest.py index a8ade97aee9..2b994b2f612 100644 --- a/services/api-server/tests/unit/conftest.py +++ b/services/api-server/tests/unit/conftest.py @@ -46,7 +46,7 @@ from simcore_service_api_server.core.application import init_app from simcore_service_api_server.core.settings import ApplicationSettings from simcore_service_api_server.db.repositories.api_keys import UserAndProductTuple -from simcore_service_api_server.services.solver_job_outputs import ResultsTypes +from simcore_service_api_server.services_http.solver_job_outputs import ResultsTypes @pytest.fixture diff --git a/services/api-server/tests/unit/test_api_solver_jobs.py b/services/api-server/tests/unit/test_api_solver_jobs.py index 4ed52877cfa..0f0e91f126f 100644 --- a/services/api-server/tests/unit/test_api_solver_jobs.py +++ b/services/api-server/tests/unit/test_api_solver_jobs.py @@ -28,7 +28,7 @@ WalletGetWithAvailableCreditsLegacy, ) from simcore_service_api_server.models.schemas.solvers import Solver -from simcore_service_api_server.services.director_v2 import ComputationTaskGet +from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet def _start_job_side_effect( diff --git a/services/api-server/tests/unit/test_exceptions.py b/services/api-server/tests/unit/test_exceptions.py index 6949e72d088..be81922bf4f 100644 --- a/services/api-server/tests/unit/test_exceptions.py +++ b/services/api-server/tests/unit/test_exceptions.py @@ -1,7 +1,7 @@ # pylint: disable=unused-variable # pylint: disable=unused-argument # pylint: disable=redefined-outer-name - +# pylint: disable=too-many-arguments from http import HTTPStatus from uuid import UUID @@ -25,8 +25,8 @@ async def test_backend_service_exception_mapper(): @service_exception_mapper( - "DummyService", - {status.HTTP_400_BAD_REQUEST: ProfileNotFoundError}, + service_name="DummyService", + http_status_map={status.HTTP_400_BAD_REQUEST: ProfileNotFoundError}, ) async def my_endpoint(status_code: int): raise HTTPStatusError( @@ -103,26 +103,26 @@ async def coro1(project_id): pass with pytest.raises(AssertionError): - _assert_correct_kwargs(func=coro1, status_map=status_map) + _assert_correct_kwargs(func=coro1, exception_types=set(status_map.values())) async def coro2(project_id=UUID("9c201eb7-ba04-4d9b-abe6-f16b406ca86d")): pass with pytest.raises(AssertionError) as exc: - _assert_correct_kwargs(func=coro2, status_map=status_map) + _assert_correct_kwargs(func=coro2, exception_types=set(status_map.values())) async def coro3(*, project_id): pass - _assert_correct_kwargs(func=coro3, status_map=status_map) + _assert_correct_kwargs(func=coro3, exception_types=set(status_map.values())) async def coro4(*, project_id=UUID("ce56af2e-e9e5-46a4-8067-662077de5528")): pass - _assert_correct_kwargs(func=coro4, status_map=status_map) + _assert_correct_kwargs(func=coro4, exception_types=set(status_map.values())) async def coro5(*, project_uuid): pass with pytest.raises(AssertionError): - _assert_correct_kwargs(func=coro5, status_map=status_map) + _assert_correct_kwargs(func=coro5, exception_types=set(status_map.values())) diff --git a/services/api-server/tests/unit/test_licensed_items.py b/services/api-server/tests/unit/test_licensed_items.py new file mode 100644 index 00000000000..e0db32dc3b7 --- /dev/null +++ b/services/api-server/tests/unit/test_licensed_items.py @@ -0,0 +1,90 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments +import asyncio + +import pytest +from fastapi import FastAPI, status +from httpx import AsyncClient, BasicAuth +from models_library.api_schemas_webserver.licensed_items import ( + LicensedItemGet as _LicensedItemGet, +) +from models_library.api_schemas_webserver.licensed_items import ( + LicensedItemGetPage as _LicensedItemGetPage, +) +from pydantic import TypeAdapter +from pytest_mock import MockerFixture +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient +from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError +from simcore_service_api_server._meta import API_VTAG +from simcore_service_api_server.api.dependencies.webserver_rpc import ( + get_wb_api_rpc_client, +) +from simcore_service_api_server.models.pagination import Page +from simcore_service_api_server.models.schemas.model_adapter import LicensedItemGet +from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient + + +@pytest.fixture +async def mock_wb_api_server_rcp( + app: FastAPI, mocker: MockerFixture, exception_to_raise: Exception | None +) -> MockerFixture: + async def _get_backend_licensed_items( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + product_name: str, + offset: int, + limit: int, + ) -> _LicensedItemGetPage: + if exception_to_raise is not None: + raise exception_to_raise + extra = _LicensedItemGet.model_config.get("json_schema_extra") + assert isinstance(extra, dict) + examples = extra.get("examples") + assert isinstance(examples, list) + return _LicensedItemGetPage( + items=[_LicensedItemGet.model_validate(ex) for ex in examples], + total=len(examples), + ) + + class DummyRpcClient: + pass + + app.dependency_overrides[get_wb_api_rpc_client] = lambda: WbApiRpcClient( + _client=DummyRpcClient() + ) + mocker.patch( + "simcore_service_api_server.services_rpc.wb_api_server._get_licensed_items", + _get_backend_licensed_items, + ) + + return mocker + + +@pytest.mark.parametrize("exception_to_raise", [None]) +async def test_get_licensed_items( + mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth +): + resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) + assert resp.status_code == status.HTTP_200_OK + TypeAdapter(Page[LicensedItemGet]).validate_json(resp.text) + + +@pytest.mark.parametrize("exception_to_raise", [asyncio.TimeoutError()]) +async def test_get_licensed_items_timeout( + mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth +): + resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) + assert resp.status_code == status.HTTP_504_GATEWAY_TIMEOUT + + +@pytest.mark.parametrize( + "exception_to_raise", + [asyncio.CancelledError(), RuntimeError(), RemoteMethodNotRegisteredError()], +) +async def test_get_licensed_items_502( + mock_wb_api_server_rcp: MockerFixture, client: AsyncClient, auth: BasicAuth +): + resp = await client.get(f"{API_VTAG}/licensed-items", auth=auth) + assert resp.status_code == status.HTTP_502_BAD_GATEWAY diff --git a/services/api-server/tests/unit/test_models_schemas_files.py b/services/api-server/tests/unit/test_models_schemas_files.py index 2ae9c4e5144..578216dd253 100644 --- a/services/api-server/tests/unit/test_models_schemas_files.py +++ b/services/api-server/tests/unit/test_models_schemas_files.py @@ -16,7 +16,7 @@ from models_library.projects_nodes_io import StorageFileID from pydantic import TypeAdapter, ValidationError from simcore_service_api_server.models.schemas.files import File -from simcore_service_api_server.services.storage import to_file_api_model +from simcore_service_api_server.services_http.storage import to_file_api_model FILE_CONTENT = "This is a test" diff --git a/services/api-server/tests/unit/test_services_directorv2.py b/services/api-server/tests/unit/test_services_directorv2.py index b88965fd755..bd5e6d9a7ae 100644 --- a/services/api-server/tests/unit/test_services_directorv2.py +++ b/services/api-server/tests/unit/test_services_directorv2.py @@ -12,7 +12,7 @@ from respx import MockRouter from settings_library.director_v2 import DirectorV2Settings from simcore_service_api_server.exceptions.backend_errors import JobNotFoundError -from simcore_service_api_server.services.director_v2 import DirectorV2Api +from simcore_service_api_server.services_http.director_v2 import DirectorV2Api @pytest.fixture diff --git a/services/api-server/tests/unit/test_services_rabbitmq.py b/services/api-server/tests/unit/test_services_rabbitmq.py index 03c0f758fca..aa644e81500 100644 --- a/services/api-server/tests/unit/test_services_rabbitmq.py +++ b/services/api-server/tests/unit/test_services_rabbitmq.py @@ -11,7 +11,7 @@ from collections.abc import AsyncIterable, Callable, Iterable from contextlib import asynccontextmanager from datetime import datetime, timedelta -from typing import Final, Literal +from typing import Final, Literal, cast from unittest.mock import AsyncMock from uuid import UUID @@ -38,11 +38,11 @@ from simcore_service_api_server.api.dependencies.rabbitmq import get_log_distributor from simcore_service_api_server.core.health_checker import get_health_checker from simcore_service_api_server.models.schemas.jobs import JobID, JobLog -from simcore_service_api_server.services.director_v2 import ( +from simcore_service_api_server.services_http.director_v2 import ( ComputationTaskGet, DirectorV2Api, ) -from simcore_service_api_server.services.log_streaming import ( +from simcore_service_api_server.services_http.log_streaming import ( LogDistributor, LogStreamer, LogStreamerRegistrationConflictError, @@ -167,7 +167,7 @@ async def _go( if log_message is None: log_message = LoggerRabbitMessage( user_id=user_id, - project_id=project_id_ or faker.uuid4(), + project_id=project_id_ or cast(UUID, faker.uuid4(cast_to=None)), node_id=node_id_, messages=messages_ or [faker.text() for _ in range(10)], log_level=level_ or logging.INFO, @@ -441,7 +441,7 @@ async def deregister(self, job_id: None): async def test_log_generator(mocker: MockFixture, faker: Faker): mocker.patch( - "simcore_service_api_server.services.log_streaming.LogStreamer._project_done", + "simcore_service_api_server.services_http.log_streaming.LogStreamer._project_done", return_value=True, ) log_streamer = LogStreamer(user_id=3, director2_api=None, job_id=None, log_distributor=_MockLogDistributor(), log_check_timeout=1) # type: ignore diff --git a/services/api-server/tests/unit/test_services_solver_job_models_converters.py b/services/api-server/tests/unit/test_services_solver_job_models_converters.py index 1016096dce5..40d60359477 100644 --- a/services/api-server/tests/unit/test_services_solver_job_models_converters.py +++ b/services/api-server/tests/unit/test_services_solver_job_models_converters.py @@ -10,7 +10,7 @@ from simcore_service_api_server.models.schemas.files import File from simcore_service_api_server.models.schemas.jobs import ArgumentTypes, Job, JobInputs from simcore_service_api_server.models.schemas.solvers import Solver -from simcore_service_api_server.services.solver_job_models_converters import ( +from simcore_service_api_server.services_http.solver_job_models_converters import ( create_job_from_project, create_job_inputs_from_node_inputs, create_jobstatus_from_task, @@ -220,7 +220,7 @@ def fake_url_for(*args, **kwargs): @pytest.mark.skip(reason="TODO: next PR") def test_create_jobstatus_from_task(): from simcore_service_api_server.models.schemas.jobs import JobStatus - from simcore_service_api_server.services.director_v2 import ComputationTaskGet + from simcore_service_api_server.services_http.director_v2 import ComputationTaskGet task = ComputationTaskGet.model_validate({}) # TODO: job_status: JobStatus = create_jobstatus_from_task(task) diff --git a/services/api-server/tests/unit/test_services_solver_job_outputs.py b/services/api-server/tests/unit/test_services_solver_job_outputs.py index b02022e5daa..01612e197e5 100644 --- a/services/api-server/tests/unit/test_services_solver_job_outputs.py +++ b/services/api-server/tests/unit/test_services_solver_job_outputs.py @@ -6,7 +6,7 @@ from typing import Union, get_args, get_origin from simcore_service_api_server.models.schemas.jobs import ArgumentTypes, File -from simcore_service_api_server.services.solver_job_outputs import ( +from simcore_service_api_server.services_http.solver_job_outputs import ( BaseFileLink, ResultsTypes, )