diff --git a/api/specs/web-server/_catalog.py b/api/specs/web-server/_catalog.py index 153c2d8b968..d861c44b0c6 100644 --- a/api/specs/web-server/_catalog.py +++ b/api/specs/web-server/_catalog.py @@ -15,7 +15,7 @@ from models_library.generics import Envelope from models_library.rest_pagination import Page from simcore_service_webserver._meta import API_VTAG -from simcore_service_webserver.catalog._handlers import ( +from simcore_service_webserver.catalog._rest_controller import ( ListServiceParams, ServicePathParams, _FromServiceOutputParams, diff --git a/api/specs/web-server/_catalog_tags.py b/api/specs/web-server/_catalog_tags.py index 26e90d952a4..bfecd6314fc 100644 --- a/api/specs/web-server/_catalog_tags.py +++ b/api/specs/web-server/_catalog_tags.py @@ -10,7 +10,7 @@ from models_library.api_schemas_webserver.catalog import CatalogServiceGet from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG -from simcore_service_webserver.catalog._tags_handlers import ( +from simcore_service_webserver.catalog._rest_tags_controller import ( ServicePathParams, ServiceTagPathParams, ) @@ -31,8 +31,7 @@ ) def list_service_tags( _path_params: Annotated[ServicePathParams, Depends()], -): - ... +): ... @router.post( @@ -41,8 +40,7 @@ def list_service_tags( ) def add_service_tag( _path_params: Annotated[ServiceTagPathParams, Depends()], -): - ... +): ... @router.post( @@ -51,5 +49,4 @@ def add_service_tag( ) def remove_service_tag( _path_params: Annotated[ServiceTagPathParams, Depends()], -): - ... +): ... diff --git a/services/web/server/src/simcore_service_webserver/catalog/client.py b/services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py similarity index 97% rename from services/web/server/src/simcore_service_webserver/catalog/client.py rename to services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py index 100b4e69318..58ee6a1f67b 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/client.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py @@ -1,7 +1,5 @@ -""" Requests to catalog service API +"""Requests to catalog service API""" -""" -import asyncio import logging import urllib.parse from collections.abc import Iterator @@ -47,7 +45,7 @@ def _handle_client_exceptions(app: web.Application) -> Iterator[ClientSession]: reason=MSG_CATALOG_SERVICE_UNAVAILABLE ) from err - except (asyncio.TimeoutError, ClientConnectionError) as err: + except (TimeoutError, ClientConnectionError) as err: _logger.debug("Request to catalog service failed: %s", err) raise web.HTTPServiceUnavailable( reason=MSG_CATALOG_SERVICE_UNAVAILABLE diff --git a/services/web/server/src/simcore_service_webserver/catalog/exceptions.py b/services/web/server/src/simcore_service_webserver/catalog/_exceptions.py similarity index 55% rename from services/web/server/src/simcore_service_webserver/catalog/exceptions.py rename to services/web/server/src/simcore_service_webserver/catalog/_exceptions.py index 11f3794661b..292bcb78d5e 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/exceptions.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_exceptions.py @@ -1,11 +1,19 @@ """Defines the different exceptions that may arise in the catalog subpackage""" +from servicelib.aiohttp import status from servicelib.rabbitmq.rpc_interfaces.catalog.errors import ( CatalogForbiddenError, CatalogItemNotFoundError, ) from ..errors import WebServerBaseError +from ..exception_handling import ( + ExceptionToHttpErrorMap, + HttpErrorInfo, + exception_handling_decorator, + to_exceptions_handlers_map, +) +from ..resource_usage.errors import DefaultPricingPlanNotFoundError class BaseCatalogError(WebServerBaseError): @@ -35,6 +43,28 @@ def __init__(self, *, service_key: str, service_version: str, **ctxs): assert CatalogItemNotFoundError # nosec +_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { + CatalogItemNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Catalog item not found", + ), + DefaultPricingPlanNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Default pricing plan not found", + ), + DefaultPricingUnitForServiceNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, "Default pricing unit not found" + ), + CatalogForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, "Forbidden catalog access" + ), +} + +handle_plugin_requests_exceptions = exception_handling_decorator( + to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) +) + + __all__: tuple[str, ...] = ( "CatalogForbiddenError", "CatalogItemNotFoundError", diff --git a/services/web/server/src/simcore_service_webserver/catalog/_handlers_errors.py b/services/web/server/src/simcore_service_webserver/catalog/_handlers_errors.py deleted file mode 100644 index 4a278cc95dc..00000000000 --- a/services/web/server/src/simcore_service_webserver/catalog/_handlers_errors.py +++ /dev/null @@ -1,31 +0,0 @@ -import functools - -from aiohttp import web -from servicelib.aiohttp.typing_extension import Handler - -from ..resource_usage.errors import DefaultPricingPlanNotFoundError -from .exceptions import ( - CatalogForbiddenError, - CatalogItemNotFoundError, - DefaultPricingUnitForServiceNotFoundError, -) - - -def reraise_catalog_exceptions_as_http_errors(handler: Handler): - @functools.wraps(handler) - async def _wrapper(request: web.Request) -> web.StreamResponse: - try: - - return await handler(request) - - except ( - CatalogItemNotFoundError, - DefaultPricingPlanNotFoundError, - DefaultPricingUnitForServiceNotFoundError, - ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except CatalogForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return _wrapper diff --git a/services/web/server/src/simcore_service_webserver/catalog/_handlers.py b/services/web/server/src/simcore_service_webserver/catalog/_rest_controller.py similarity index 86% rename from services/web/server/src/simcore_service_webserver/catalog/_handlers.py rename to services/web/server/src/simcore_service_webserver/catalog/_rest_controller.py index cdc617c5db3..9a83748fd8c 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_rest_controller.py @@ -1,4 +1,4 @@ -""" rest api handlers +"""rest api handlers - Take into account that part of the API is also needed in the public API so logic should live in the catalog service in his final version @@ -7,7 +7,6 @@ import asyncio import logging -import urllib.parse from typing import Final from aiohttp import web @@ -26,7 +25,7 @@ ServiceResourcesDict, ServiceResourcesDictHelpers, ) -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, Field from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -39,9 +38,16 @@ from ..resource_usage.service import get_default_service_pricing_plan from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response -from . import _api, _handlers_errors, client -from ._api import CatalogRequestContext -from .exceptions import DefaultPricingUnitForServiceNotFoundError +from . import _catalog_rest_client_service, _service +from ._exceptions import ( + DefaultPricingUnitForServiceNotFoundError, + handle_plugin_requests_exceptions, +) +from .controller_rest_schemas import ( + CatalogRequestContext, + ListServiceParams, + ServicePathParams, +) _logger = logging.getLogger(__name__) @@ -51,41 +57,20 @@ routes = RouteTableDef() -class ServicePathParams(BaseModel): - service_key: ServiceKey - service_version: ServiceVersion - model_config = ConfigDict( - populate_by_name=True, - extra="forbid", - ) - - @field_validator("service_key", mode="before") - @classmethod - def ensure_unquoted(cls, v): - # NOTE: this is needed as in pytest mode, the aiohttp server does not seem to unquote automatically - if v is not None: - return urllib.parse.unquote(v) - return v - - -class ListServiceParams(PageQueryParameters): - ... - - @routes.get( f"{VTAG}/catalog/services/-/latest", name="list_services_latest", ) @login_required @permission_required("services.catalog.*") -@_handlers_errors.reraise_catalog_exceptions_as_http_errors +@handle_plugin_requests_exceptions async def list_services_latest(request: Request): request_ctx = CatalogRequestContext.create(request) query_params: ListServiceParams = parse_request_query_parameters_as( ListServiceParams, request ) - page_items, page_meta = await _api.list_latest_services( + page_items, page_meta = await _service.list_latest_services( request.app, user_id=request_ctx.user_id, product_name=request_ctx.product_name, @@ -116,7 +101,7 @@ async def list_services_latest(request: Request): ) @login_required @permission_required("services.catalog.*") -@_handlers_errors.reraise_catalog_exceptions_as_http_errors +@handle_plugin_requests_exceptions async def get_service(request: Request): request_ctx = CatalogRequestContext.create(request) path_params = parse_request_path_parameters_as(ServicePathParams, request) @@ -124,7 +109,7 @@ async def get_service(request: Request): assert request_ctx # nosec assert path_params # nosec - service = await _api.get_service_v2( + service = await _service.get_service_v2( request.app, user_id=request_ctx.user_id, product_name=request_ctx.product_name, @@ -142,7 +127,7 @@ async def get_service(request: Request): ) @login_required @permission_required("services.catalog.*") -@_handlers_errors.reraise_catalog_exceptions_as_http_errors +@handle_plugin_requests_exceptions async def update_service(request: Request): request_ctx = CatalogRequestContext.create(request) path_params = parse_request_path_parameters_as(ServicePathParams, request) @@ -154,7 +139,7 @@ async def update_service(request: Request): assert path_params # nosec assert update # nosec - updated = await _api.update_service_v2( + updated = await _service.update_service_v2( request.app, user_id=request_ctx.user_id, product_name=request_ctx.product_name, @@ -178,7 +163,7 @@ async def list_service_inputs(request: Request): path_params = parse_request_path_parameters_as(ServicePathParams, request) # Evaluate and return validated model - response_model = await _api.list_service_inputs( + response_model = await _service.list_service_inputs( path_params.service_key, path_params.service_version, ctx ) @@ -203,7 +188,7 @@ async def get_service_input(request: Request): path_params = parse_request_path_parameters_as(_ServiceInputsPathParams, request) # Evaluate and return validated model - response_model = await _api.get_service_input( + response_model = await _service.get_service_input( path_params.service_key, path_params.service_version, path_params.input_key, @@ -236,7 +221,7 @@ async def get_compatible_inputs_given_source_output(request: Request): ) # Evaluate and return validated model - data = await _api.get_compatible_inputs_given_source_output( + data = await _service.get_compatible_inputs_given_source_output( path_params.service_key, path_params.service_version, query_params.from_service_key, @@ -261,7 +246,7 @@ async def list_service_outputs(request: Request): path_params = parse_request_path_parameters_as(ServicePathParams, request) # Evaluate and return validated model - response_model = await _api.list_service_outputs( + response_model = await _service.list_service_outputs( path_params.service_key, path_params.service_version, ctx ) @@ -286,7 +271,7 @@ async def get_service_output(request: Request): path_params = parse_request_path_parameters_as(_ServiceOutputsPathParams, request) # Evaluate and return validated model - response_model = await _api.get_service_output( + response_model = await _service.get_service_output( path_params.service_key, path_params.service_version, path_params.output_key, @@ -323,7 +308,7 @@ async def get_compatible_outputs_given_target_input(request: Request): _ToServiceInputsParams, request ) - data = await _api.get_compatible_outputs_given_target_input( + data = await _service.get_compatible_outputs_given_target_input( path_params.service_key, path_params.service_version, query_params.to_service_key, @@ -351,11 +336,13 @@ async def get_service_resources(request: Request): """ ctx = CatalogRequestContext.create(request) path_params = parse_request_path_parameters_as(ServicePathParams, request) - service_resources: ServiceResourcesDict = await client.get_service_resources( - request.app, - user_id=ctx.user_id, - service_key=path_params.service_key, - service_version=path_params.service_version, + service_resources: ServiceResourcesDict = ( + await _catalog_rest_client_service.get_service_resources( + request.app, + user_id=ctx.user_id, + service_key=path_params.service_key, + service_version=path_params.service_version, + ) ) data = ServiceResourcesDictHelpers.create_jsonable(service_resources) @@ -370,7 +357,7 @@ async def get_service_resources(request: Request): ) @login_required @permission_required("services.catalog.*") -@_handlers_errors.reraise_catalog_exceptions_as_http_errors +@handle_plugin_requests_exceptions async def get_service_pricing_plan(request: Request): ctx = CatalogRequestContext.create(request) path_params = parse_request_path_parameters_as(ServicePathParams, request) diff --git a/services/web/server/src/simcore_service_webserver/catalog/_tags_handlers.py b/services/web/server/src/simcore_service_webserver/catalog/_rest_tags_controller.py similarity index 91% rename from services/web/server/src/simcore_service_webserver/catalog/_tags_handlers.py rename to services/web/server/src/simcore_service_webserver/catalog/_rest_tags_controller.py index dc75617f497..199f8b4886f 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_tags_handlers.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_rest_tags_controller.py @@ -1,21 +1,16 @@ import logging from aiohttp import web -from models_library.basic_types import IdInt from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as from .._meta import API_VTAG from ..login.decorators import login_required from ..security.decorators import permission_required -from ._handlers import ServicePathParams +from .controller_rest_schemas import ServicePathParams, ServiceTagPathParams _logger = logging.getLogger(__name__) -class ServiceTagPathParams(ServicePathParams): - tag_id: IdInt - - routes = web.RouteTableDef() diff --git a/services/web/server/src/simcore_service_webserver/catalog/_api.py b/services/web/server/src/simcore_service_webserver/catalog/_service.py similarity index 84% rename from services/web/server/src/simcore_service_webserver/catalog/_api.py rename to services/web/server/src/simcore_service_webserver/catalog/_service.py index beec1366857..823bf033b3a 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_api.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_service.py @@ -3,7 +3,6 @@ from typing import Any, cast from aiohttp import web -from aiohttp.web import Request from models_library.api_schemas_catalog.services import MyServiceGet, ServiceUpdateV2 from models_library.api_schemas_webserver.catalog import ( ServiceInputGet, @@ -22,45 +21,23 @@ from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder from pint import UnitRegistry -from pydantic import BaseModel, ConfigDict -from servicelib.aiohttp.requests_validation import handle_validation_as_http_error from servicelib.rabbitmq._errors import RPCServerError from servicelib.rabbitmq.rpc_interfaces.catalog import services as catalog_rpc from servicelib.rabbitmq.rpc_interfaces.catalog.errors import CatalogNotAvailableError from servicelib.rest_constants import RESPONSE_MODEL_POLICY -from ..constants import RQ_PRODUCT_KEY, RQT_USERID_KEY from ..rabbitmq import get_rabbitmq_rpc_client -from . import client -from ._api_units import can_connect, replace_service_input_outputs -from ._models import ServiceInputGetFactory, ServiceOutputGetFactory +from . import _catalog_rest_client_service +from ._units_service import can_connect, replace_service_input_outputs +from .controller_rest_schemas import ( + CatalogRequestContext, + ServiceInputGetFactory, + ServiceOutputGetFactory, +) _logger = logging.getLogger(__name__) -class CatalogRequestContext(BaseModel): - app: web.Application - user_id: UserID - product_name: str - unit_registry: UnitRegistry - model_config = ConfigDict(arbitrary_types_allowed=True) - - @classmethod - def create(cls, request: Request) -> "CatalogRequestContext": - with handle_validation_as_http_error( - error_msg_template="Invalid request", - resource_name=request.rel_url.path, - use_error_v1=True, - ): - assert request.app # nosec - return cls( - app=request.app, - user_id=request[RQT_USERID_KEY], - product_name=request[RQ_PRODUCT_KEY], - unit_registry=request.app[UnitRegistry.__name__], - ) - - async def _safe_replace_service_input_outputs( service: dict[str, Any], unit_registry: UnitRegistry ): @@ -190,26 +167,10 @@ async def update_service_v2( return data -async def list_services( - app: web.Application, - *, - user_id: UserID, - product_name: str, - unit_registry: UnitRegistry, -): - services = await client.get_services_for_user_in_product( - app, user_id, product_name, only_key_versions=False - ) - for service in services: - await _safe_replace_service_input_outputs(service, unit_registry) - - return services - - async def list_service_inputs( service_key: ServiceKey, service_version: ServiceVersion, ctx: CatalogRequestContext ) -> list[ServiceInputGet]: - service = await client.get_service( + service = await _catalog_rest_client_service.get_service( ctx.app, ctx.user_id, service_key, service_version, ctx.product_name ) return [ @@ -226,7 +187,7 @@ async def get_service_input( input_key: ServiceInputKey, ctx: CatalogRequestContext, ) -> ServiceInputGet: - service = await client.get_service( + service = await _catalog_rest_client_service.get_service( ctx.app, ctx.user_id, service_key, service_version, ctx.product_name ) service_input: ServiceInputGet = ( @@ -285,7 +246,7 @@ async def list_service_outputs( service_version: ServiceVersion, ctx: CatalogRequestContext, ) -> list[ServiceOutputGet]: - service = await client.get_service( + service = await _catalog_rest_client_service.get_service( ctx.app, ctx.user_id, service_key, service_version, ctx.product_name ) return [ @@ -302,7 +263,7 @@ async def get_service_output( output_key: ServiceOutputKey, ctx: CatalogRequestContext, ) -> ServiceOutputGet: - service = await client.get_service( + service = await _catalog_rest_client_service.get_service( ctx.app, ctx.user_id, service_key, service_version, ctx.product_name ) return cast( # mypy -> aiocache is not typed. diff --git a/services/web/server/src/simcore_service_webserver/catalog/_api_units.py b/services/web/server/src/simcore_service_webserver/catalog/_units_service.py similarity index 97% rename from services/web/server/src/simcore_service_webserver/catalog/_api_units.py rename to services/web/server/src/simcore_service_webserver/catalog/_units_service.py index 65e435f6886..9cb3c3269b0 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_api_units.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_units_service.py @@ -3,7 +3,11 @@ from models_library.services import BaseServiceIOModel, ServiceInput, ServiceOutput from pint import PintError, UnitRegistry -from ._models import ServiceInputGetFactory, ServiceOutputGetFactory, get_unit_name +from .controller_rest_schemas import ( + ServiceInputGetFactory, + ServiceOutputGetFactory, + get_unit_name, +) def _get_type_name(port: BaseServiceIOModel) -> str: diff --git a/services/web/server/src/simcore_service_webserver/catalog/catalog_service.py b/services/web/server/src/simcore_service_webserver/catalog/catalog_service.py index e0dab71aaf2..33b92d864a3 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/catalog_service.py +++ b/services/web/server/src/simcore_service_webserver/catalog/catalog_service.py @@ -1,5 +1,19 @@ -from ._api import batch_get_my_services +from ._catalog_rest_client_service import ( # noqa + get_service, + get_service_access_rights, + get_service_resources, + get_services_for_user_in_product, + is_catalog_service_responsive, + to_backend_service, +) +from ._service import batch_get_my_services # noqa -__all__: tuple[str, ...] = ("batch_get_my_services",) - -# nopycln: file +__all__: tuple[str, ...] = ( + "is_catalog_service_responsive", + "to_backend_service", + "get_services_for_user_in_product", + "get_service", + "get_service_resources", + "get_service_access_rights", + "batch_get_my_services", +) diff --git a/services/web/server/src/simcore_service_webserver/catalog/_models.py b/services/web/server/src/simcore_service_webserver/catalog/controller_rest_schemas.py similarity index 68% rename from services/web/server/src/simcore_service_webserver/catalog/_models.py rename to services/web/server/src/simcore_service_webserver/catalog/controller_rest_schemas.py index af137ba11d8..9d594f8954a 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_models.py +++ b/services/web/server/src/simcore_service_webserver/catalog/controller_rest_schemas.py @@ -1,17 +1,31 @@ import logging +import urllib.parse from collections.abc import Callable from dataclasses import dataclass from typing import Any, Final from aiocache import cached # type: ignore[import-untyped] +from aiohttp import web +from aiohttp.web import Request from models_library.api_schemas_webserver.catalog import ( ServiceInputGet, ServiceInputKey, ServiceOutputGet, ServiceOutputKey, ) -from models_library.services import BaseServiceIOModel +from models_library.basic_types import IdInt +from models_library.rest_pagination import PageQueryParameters +from models_library.services import BaseServiceIOModel, ServiceKey, ServiceVersion +from models_library.users import UserID from pint import PintError, Quantity, UnitRegistry +from pydantic import ( + BaseModel, + ConfigDict, + field_validator, +) +from servicelib.aiohttp.requests_validation import handle_validation_as_http_error + +from ..constants import RQ_PRODUCT_KEY, RQT_USERID_KEY _logger = logging.getLogger(__name__) @@ -131,3 +145,50 @@ async def from_catalog_service_api_model( ) return port + + +class CatalogRequestContext(BaseModel): + app: web.Application + user_id: UserID + product_name: str + unit_registry: UnitRegistry + model_config = ConfigDict(arbitrary_types_allowed=True) + + @classmethod + def create(cls, request: Request) -> "CatalogRequestContext": + with handle_validation_as_http_error( + error_msg_template="Invalid request", + resource_name=request.rel_url.path, + use_error_v1=True, + ): + assert request.app # nosec + return cls( + app=request.app, + user_id=request[RQT_USERID_KEY], + product_name=request[RQ_PRODUCT_KEY], + unit_registry=request.app[UnitRegistry.__name__], + ) + + +class ServicePathParams(BaseModel): + service_key: ServiceKey + service_version: ServiceVersion + model_config = ConfigDict( + populate_by_name=True, + extra="forbid", + ) + + @field_validator("service_key", mode="before") + @classmethod + def ensure_unquoted(cls, v): + # NOTE: this is needed as in pytest mode, the aiohttp server does not seem to unquote automatically + if v is not None: + return urllib.parse.unquote(v) + return v + + +class ListServiceParams(PageQueryParameters): ... + + +class ServiceTagPathParams(ServicePathParams): + tag_id: IdInt diff --git a/services/web/server/src/simcore_service_webserver/catalog/plugin.py b/services/web/server/src/simcore_service_webserver/catalog/plugin.py index 2af8da917f0..250b3810d73 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/plugin.py +++ b/services/web/server/src/simcore_service_webserver/catalog/plugin.py @@ -1,6 +1,4 @@ -""" Subsystem to communicate with catalog service - -""" +"""Subsystem to communicate with catalog service""" import logging @@ -8,7 +6,7 @@ from pint import UnitRegistry from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup -from . import _handlers, _tags_handlers +from . import _rest_controller, _rest_tags_controller _logger = logging.getLogger(__name__) @@ -24,11 +22,11 @@ def setup_catalog(app: web.Application): # ensures routes are names that corresponds to function names assert all( # nosec route_def.kwargs["name"] == route_def.handler.__name__ # type: ignore[attr-defined] # route_def is a RouteDef not an Abstract - for route_def in _handlers.routes + for route_def in _rest_controller.routes ) - app.add_routes(_handlers.routes) - app.add_routes(_tags_handlers.routes) + app.add_routes(_rest_controller.routes) + app.add_routes(_rest_tags_controller.routes) # prepares units registry app[UnitRegistry.__name__] = UnitRegistry() diff --git a/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py b/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py index bed2f77f7f2..214fafff773 100644 --- a/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/diagnostics/_handlers.py @@ -1,6 +1,4 @@ -""" Handler functions and routing for diagnostics - -""" +"""Handler functions and routing for diagnostics""" import asyncio import logging @@ -15,7 +13,7 @@ from servicelib.utils import logged_gather from .._meta import API_VERSION, APP_NAME, api_version_prefix -from ..catalog.client import is_catalog_service_responsive +from ..catalog import catalog_service from ..db import plugin from ..director_v2 import api as director_v2_api from ..login.decorators import login_required @@ -131,7 +129,7 @@ async def _check_director2(): async def _check_catalog(): check.services["catalog"] = { - "healthy": await is_catalog_service_responsive(request.app) + "healthy": await catalog_service.is_catalog_service_responsive(request.app) } async def _check_resource_usage_tracker(): diff --git a/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py b/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py index 62f02f2b1d1..d605ada5e59 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py +++ b/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py @@ -7,7 +7,7 @@ from aiohttp import web from servicelib.pools import non_blocking_process_pool_executor -from ...catalog.client import get_service +from ...catalog import catalog_service from ...projects.exceptions import BaseProjectError from ...projects.models import ProjectDict from ...projects.projects_service import get_project_for_user @@ -183,7 +183,7 @@ async def create_sds_directory( service_version = entry["version"] label = entry["label"] - service_data = await get_service( + service_data = await catalog_service.get_service( app=app, user_id=user_id, service_key=service_key, diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py index 4a93ad9286b..711b5b0f64c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py @@ -27,7 +27,7 @@ from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB from ..application_settings import get_application_settings -from ..catalog import client as catalog_client +from ..catalog import catalog_service from ..director_v2 import api as director_v2_api from ..dynamic_scheduler import api as dynamic_scheduler_api from ..folders import _folders_repository as _folders_repository @@ -219,7 +219,7 @@ async def _compose_project_data( NodeID(node_id): ProjectNodeCreate( node_id=NodeID(node_id), required_resources=jsonable_encoder( - await catalog_client.get_service_resources( + await catalog_service.get_service_resources( app, user_id, node_data["key"], node_data["version"] ) ), @@ -419,9 +419,9 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche user_specific_project_data_db = ( await _projects_repository.get_user_specific_project_data_db( project_uuid=new_project["uuid"], - private_workspace_user_id_or_none=user_id - if workspace_id is None - else None, + private_workspace_user_id_or_none=( + user_id if workspace_id is None else None + ), ) ) new_project["folderId"] = user_specific_project_data_db.folder_id diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index d83d2265bfe..5965a7e4a2a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -1,4 +1,4 @@ -""" Utils to implement READ operations (from cRud) on the project resource +"""Utils to implement READ operations (from cRud) on the project resource Read operations are list, get @@ -19,7 +19,7 @@ from simcore_postgres_database.models.projects import ProjectType from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB -from ..catalog.client import get_services_for_user_in_product +from ..catalog import catalog_service from ..folders import _folders_repository from ..workspaces._workspaces_service import check_user_workspace_access from . import projects_service @@ -104,8 +104,10 @@ async def list_projects( # pylint: disable=too-many-arguments ) -> tuple[list[ProjectDict], int]: db = ProjectDBAPI.get_from_app_context(app) - user_available_services: list[dict] = await get_services_for_user_in_product( - app, user_id, product_name, only_key_versions=True + user_available_services: list[dict] = ( + await catalog_service.get_services_for_user_in_product( + app, user_id, product_name, only_key_versions=True + ) ) workspace_is_private = True @@ -184,8 +186,10 @@ async def list_projects_full_depth( ) -> tuple[list[ProjectDict], int]: db = ProjectDBAPI.get_from_app_context(app) - user_available_services: list[dict] = await get_services_for_user_in_product( - app, user_id, product_name, only_key_versions=True + user_available_services: list[dict] = ( + await catalog_service.get_services_for_user_in_product( + app, user_id, product_name, only_key_versions=True + ) ) db_projects, db_project_types, total_number_projects = await db.list_projects_dicts( diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index 6c41e94384a..2868d1a3cf0 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -34,7 +34,7 @@ from servicelib.redis import get_project_locked_state from .._meta import API_VTAG as VTAG -from ..catalog.client import get_services_for_user_in_product +from ..catalog import catalog_service from ..login.decorators import login_required from ..redis import get_redis_lock_manager_client_sdk from ..resource_manager.user_sessions import PROJECT_ID_KEY, managed_resource @@ -97,11 +97,11 @@ async def create_project(request: web.Request): predefined_project = None else: # request w/ body (I found cases in which body = {}) - project_create: ( - ProjectCreateNew | ProjectCopyOverride | EmptyModel - ) = await parse_request_body_as( - ProjectCreateNew | ProjectCopyOverride | EmptyModel, # type: ignore[arg-type] # from pydantic v2 --> https://github.com/pydantic/pydantic/discussions/4950 - request, + project_create: ProjectCreateNew | ProjectCopyOverride | EmptyModel = ( + await parse_request_body_as( + ProjectCreateNew | ProjectCopyOverride | EmptyModel, # type: ignore[arg-type] # from pydantic v2 --> https://github.com/pydantic/pydantic/discussions/4950 + request, + ) ) predefined_project = project_create.to_domain_model() or None @@ -280,8 +280,10 @@ async def get_project(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) - user_available_services: list[dict] = await get_services_for_user_in_product( - request.app, req_ctx.user_id, req_ctx.product_name, only_key_versions=True + user_available_services: list[dict] = ( + await catalog_service.get_services_for_user_in_product( + request.app, req_ctx.user_id, req_ctx.product_name, only_key_versions=True + ) ) project = await projects_service.get_project_for_user( diff --git a/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py index 986090a224a..9c97f90b995 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py @@ -58,7 +58,6 @@ from .._meta import API_VTAG as VTAG from ..catalog import catalog_service -from ..catalog import client as catalog_client from ..dynamic_scheduler import api as dynamic_scheduler_api from ..groups.api import get_group_from_gid, list_all_user_groups_ids from ..groups.exceptions import GroupNotFoundError @@ -539,7 +538,7 @@ async def get_project_services_access_for_gid(request: web.Request) -> web.Respo project_services_access_rights: list[ServiceAccessRightsGet] = await asyncio.gather( *[ - catalog_client.get_service_access_rights( + catalog_service.get_service_access_rights( app=request.app, user_id=req_ctx.user_id, service_key=service.key, diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_service.py b/services/web/server/src/simcore_service_webserver/projects/projects_service.py index 76ba458b3ff..46fb9b8d0e7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_service.py @@ -94,7 +94,7 @@ from simcore_postgres_database.webserver_models import ProjectType from ..application_settings import get_application_settings -from ..catalog import client as catalog_client +from ..catalog import catalog_service from ..director_v2 import api as director_v2_api from ..dynamic_scheduler import api as dynamic_scheduler_api from ..products import products_web @@ -412,9 +412,9 @@ async def _get_default_pricing_and_hardware_info( ) -_MACHINE_TOTAL_RAM_SAFE_MARGIN_RATIO: Final[ - float -] = 0.1 # NOTE: machines always have less available RAM than advertised +_MACHINE_TOTAL_RAM_SAFE_MARGIN_RATIO: Final[float] = ( + 0.1 # NOTE: machines always have less available RAM than advertised +) _SIDECARS_OPS_SAFE_RAM_MARGIN: Final[ByteSize] = TypeAdapter(ByteSize).validate_python( "1GiB" ) @@ -437,11 +437,11 @@ async def update_project_node_resources_from_hardware_info( return try: rabbitmq_rpc_client = get_rabbitmq_rpc_client(app) - unordered_list_ec2_instance_types: list[ - EC2InstanceTypeGet - ] = await get_instance_type_details( - rabbitmq_rpc_client, - instance_type_names=set(hardware_info.aws_ec2_instances), + unordered_list_ec2_instance_types: list[EC2InstanceTypeGet] = ( + await get_instance_type_details( + rabbitmq_rpc_client, + instance_type_names=set(hardware_info.aws_ec2_instances), + ) ) assert unordered_list_ec2_instance_types # nosec @@ -817,7 +817,7 @@ async def add_project_node( ) node_uuid = NodeID(service_id if service_id else f"{uuid4()}") - default_resources = await catalog_client.get_service_resources( + default_resources = await catalog_service.get_service_resources( request.app, user_id, service_key, service_version ) db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) @@ -1366,10 +1366,10 @@ async def _open_project() -> bool: # Assign project_id to current_session current_session: UserSessionID = user_session.get_id() - sessions_with_project: list[ - UserSessionID - ] = await user_session.find_users_of_resource( - app, PROJECT_ID_KEY, f"{project_uuid}" + sessions_with_project: list[UserSessionID] = ( + await user_session.find_users_of_resource( + app, PROJECT_ID_KEY, f"{project_uuid}" + ) ) if not sessions_with_project: # no one has the project so we assign it @@ -1418,10 +1418,10 @@ async def try_close_project_for_user( ): with managed_resource(user_id, client_session_id, app) as user_session: current_session: UserSessionID = user_session.get_id() - all_sessions_with_project: list[ - UserSessionID - ] = await user_session.find_users_of_resource( - app, key=PROJECT_ID_KEY, value=project_uuid + all_sessions_with_project: list[UserSessionID] = ( + await user_session.find_users_of_resource( + app, key=PROJECT_ID_KEY, value=project_uuid + ) ) # first check whether other sessions registered this project @@ -1616,7 +1616,7 @@ async def is_service_deprecated( service_version: str, product_name: str, ) -> bool: - service = await catalog_client.get_service( + service = await catalog_service.get_service( app, user_id, service_key, service_version, product_name ) if deprecation_date := service.get("deprecated"): @@ -1665,7 +1665,7 @@ async def get_project_node_resources( ) if not node_resources: # get default resources - node_resources = await catalog_client.get_service_resources( + node_resources = await catalog_service.get_service_resources( app, user_id, service_key, service_version ) return node_resources @@ -1696,7 +1696,7 @@ async def update_project_node_resources( if not current_resources: # NOTE: this can happen after the migration # get default resources - current_resources = await catalog_client.get_service_resources( + current_resources = await catalog_service.get_service_resources( app, user_id, service_key, service_version ) diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_service.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_service.py index f446e089aef..d74bdee870a 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_service.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_service.py @@ -24,7 +24,7 @@ pricing_units, ) -from ..catalog import client as catalog_client +from ..catalog import catalog_service from ..rabbitmq import get_rabbitmq_rpc_client ## Pricing Plans @@ -146,10 +146,10 @@ async def list_connected_services_to_pricing_plan( app: web.Application, product_name: ProductName, pricing_plan_id: PricingPlanId ) -> list[PricingPlanToServiceGet]: rpc_client = get_rabbitmq_rpc_client(app) - output: list[ - PricingPlanToServiceGet - ] = await pricing_plans.list_connected_services_to_pricing_plan_by_pricing_plan( - rpc_client, product_name=product_name, pricing_plan_id=pricing_plan_id + output: list[PricingPlanToServiceGet] = ( + await pricing_plans.list_connected_services_to_pricing_plan_by_pricing_plan( + rpc_client, product_name=product_name, pricing_plan_id=pricing_plan_id + ) ) return output @@ -163,7 +163,7 @@ async def connect_service_to_pricing_plan( service_version: ServiceVersion, ) -> PricingPlanToServiceGet: # Check whether service key and version exists - await catalog_client.get_service( + await catalog_service.get_service( app, user_id, service_key, service_version, product_name ) diff --git a/services/web/server/tests/unit/isolated/test_catalog_api_units.py b/services/web/server/tests/unit/isolated/test_catalog_api_units.py index 39d1824a775..3fa04b3a933 100644 --- a/services/web/server/tests/unit/isolated/test_catalog_api_units.py +++ b/services/web/server/tests/unit/isolated/test_catalog_api_units.py @@ -9,7 +9,7 @@ from models_library.function_services_catalog.services import demo_units from models_library.services import ServiceInput, ServiceOutput from pint import UnitRegistry -from simcore_service_webserver.catalog._api_units import can_connect +from simcore_service_webserver.catalog._units_service import can_connect def _create_port_data(schema: dict[str, Any]): diff --git a/services/web/server/tests/unit/isolated/test_catalog_models.py b/services/web/server/tests/unit/isolated/test_catalog_models.py index ec82b0ab367..d42ed45e603 100644 --- a/services/web/server/tests/unit/isolated/test_catalog_models.py +++ b/services/web/server/tests/unit/isolated/test_catalog_models.py @@ -9,8 +9,10 @@ import pytest from pint import UnitRegistry from pytest_benchmark.fixture import BenchmarkFixture -from simcore_service_webserver.catalog._api_units import replace_service_input_outputs -from simcore_service_webserver.catalog._handlers import RESPONSE_MODEL_POLICY +from simcore_service_webserver.catalog._rest_controller import RESPONSE_MODEL_POLICY +from simcore_service_webserver.catalog._units_service import ( + replace_service_input_outputs, +) @pytest.fixture(params=["UnitRegistry", None]) diff --git a/services/web/server/tests/unit/isolated/test_catalog_setup.py b/services/web/server/tests/unit/isolated/test_catalog_setup.py index 2fdd2e336ef..f16efc1695e 100644 --- a/services/web/server/tests/unit/isolated/test_catalog_setup.py +++ b/services/web/server/tests/unit/isolated/test_catalog_setup.py @@ -9,7 +9,7 @@ from aiohttp.test_utils import TestClient from servicelib.aiohttp.application import create_safe_application from simcore_service_webserver._meta import api_version_prefix -from simcore_service_webserver.catalog.client import to_backend_service +from simcore_service_webserver.catalog import catalog_service from simcore_service_webserver.catalog.plugin import setup_catalog from yarl import URL @@ -35,6 +35,8 @@ def test_url_translation(): assert rel_url.path.startswith(f"/{api_version_prefix}/catalog") api_target_origin = URL("http://catalog:8000") - api_target_url = to_backend_service(rel_url, api_target_origin, "v5") + api_target_url = catalog_service.to_backend_service( + rel_url, api_target_origin, "v5" + ) assert str(api_target_url) == "http://catalog:8000/v5/dags/123?page_size=6" diff --git a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py index f1024c85af3..b5ca8665c37 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py +++ b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py @@ -2,12 +2,14 @@ # pylint:disable=unused-argument # pylint:disable=redefined-outer-name +import re import urllib.parse from unittest.mock import MagicMock import pytest from aiohttp import web from aiohttp.test_utils import TestClient +from aioresponses import aioresponses as AioResponsesMock from faker import Faker from models_library.api_schemas_catalog.services import ServiceGetV2 from models_library.api_schemas_webserver.catalog import ( @@ -28,6 +30,10 @@ from pytest_simcore.helpers.typing_env import EnvVarsDict from pytest_simcore.helpers.webserver_login import UserInfoDict from servicelib.aiohttp import status +from simcore_service_webserver.catalog.controller_rest_schemas import ( + ServiceInputGet, + ServiceOutputGet, +) from simcore_service_webserver.db.models import UserRole @@ -111,17 +117,17 @@ async def _update( return { "list_services_paginated": mocker.patch( - "simcore_service_webserver.catalog._api.catalog_rpc.list_services_paginated", + "simcore_service_webserver.catalog._service.catalog_rpc.list_services_paginated", autospec=True, side_effect=_list, ), "get_service": mocker.patch( - "simcore_service_webserver.catalog._api.catalog_rpc.get_service", + "simcore_service_webserver.catalog._service.catalog_rpc.get_service", autospec=True, side_effect=_get, ), "update_service": mocker.patch( - "simcore_service_webserver.catalog._api.catalog_rpc.update_service", + "simcore_service_webserver.catalog._service.catalog_rpc.update_service", autospec=True, side_effect=_update, ), @@ -156,6 +162,196 @@ async def test_list_services_latest( assert mocked_rpc_catalog_service_api["list_services_paginated"].call_count == 1 +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_list_inputs( + client: TestClient, logged_user: UserInfoDict, aioresponses_mocker: AioResponsesMock +): + + url_pattern = re.compile(r"http://catalog:8000/v0/services/.*") + service_payload = ServiceGetV2.model_json_schema()["examples"][0] + aioresponses_mocker.get( + url_pattern, + status=status.HTTP_200_OK, + payload=service_payload, + ) + + service_key = "simcore/services/comp/itis/sleeper" + service_version = "0.1.0" + assert client.app and client.app.router + url = client.app.router["list_service_inputs"].url_for( + service_key=urllib.parse.quote(service_key, safe=""), + service_version=service_version, + ) + + response = await client.get(f"{url}") + data, _ = await assert_status(response, status.HTTP_200_OK) + TypeAdapter(list[ServiceInputGet]).validate_python(data) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_list_outputs( + client: TestClient, logged_user: UserInfoDict, aioresponses_mocker: AioResponsesMock +): + + url_pattern = re.compile(r"http://catalog:8000/v0/services/.*") + service_payload = ServiceGetV2.model_json_schema()["examples"][0] + aioresponses_mocker.get( + url_pattern, + status=status.HTTP_200_OK, + payload=service_payload, + ) + + service_key = "simcore/services/comp/itis/sleeper" + service_version = "0.1.0" + assert client.app and client.app.router + url = client.app.router["list_service_outputs"].url_for( + service_key=urllib.parse.quote(service_key, safe=""), + service_version=service_version, + ) + + response = await client.get(f"{url}") + data, _ = await assert_status(response, status.HTTP_200_OK) + TypeAdapter(list[ServiceOutputGet]).validate_python(data) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_get_outputs( + client: TestClient, logged_user: UserInfoDict, aioresponses_mocker: AioResponsesMock +): + + url_pattern = re.compile(r"http://catalog:8000/v0/services/.*") + service_payload = ServiceGetV2.model_json_schema()["examples"][0] + aioresponses_mocker.get( + url_pattern, + status=status.HTTP_200_OK, + payload=service_payload, + ) + + service_key = "simcore/services/comp/itis/sleeper" + service_version = "0.1.0" + assert client.app and client.app.router + url = client.app.router["get_service_output"].url_for( + service_key=urllib.parse.quote(service_key, safe=""), + service_version=service_version, + output_key=next(iter(service_payload["outputs"].keys())), + ) + + response = await client.get(f"{url}") + data, _ = await assert_status(response, status.HTTP_200_OK) + ServiceOutputGet.model_validate(data) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_get_inputs( + client: TestClient, logged_user: UserInfoDict, aioresponses_mocker: AioResponsesMock +): + url_pattern = re.compile(r"http://catalog:8000/v0/services/.*") + service_payload = ServiceGetV2.model_json_schema()["examples"][0] + aioresponses_mocker.get( + url_pattern, + status=status.HTTP_200_OK, + payload=service_payload, + ) + + service_key = "simcore/services/comp/itis/sleeper" + service_version = "0.1.0" + assert client.app and client.app.router + url = client.app.router["get_service_input"].url_for( + service_key=urllib.parse.quote(service_key, safe=""), + service_version=service_version, + input_key=next(iter(service_payload["inputs"].keys())), + ) + response = await client.get(f"{url}") + data, _ = await assert_status(response, status.HTTP_200_OK) + ServiceInputGet.model_validate(data) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_get_compatible_inputs_given_source_outputs( + client: TestClient, logged_user: UserInfoDict, aioresponses_mocker: AioResponsesMock +): + url_pattern = re.compile(r"http://catalog:8000/v0/services/.*") + service_payload = ServiceGetV2.model_json_schema()["examples"][0] + for _ in range(2): + aioresponses_mocker.get( + url_pattern, + status=status.HTTP_200_OK, + payload=service_payload, + ) + + service_key = "simcore/services/comp/itis/sleeper" + service_version = "0.1.0" + assert client.app and client.app.router + url = ( + client.app.router["get_compatible_inputs_given_source_output"] + .url_for( + service_key=urllib.parse.quote(service_key, safe=""), + service_version=service_version, + ) + .with_query( + { + "fromService": "simcore/services/comp/itis/sleeper", + "fromVersion": "0.1.0", + "fromOutput": "output_1", + } + ) + ) + response = await client.get(f"{url}") + _, _ = await assert_status(response, status.HTTP_200_OK) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_get_compatible_outputs_given_target_inptuts( + client: TestClient, logged_user: UserInfoDict, aioresponses_mocker: AioResponsesMock +): + url_pattern = re.compile(r"http://catalog:8000/v0/services/.*") + service_payload = ServiceGetV2.model_json_schema()["examples"][0] + for _ in range(2): + aioresponses_mocker.get( + url_pattern, + status=status.HTTP_200_OK, + payload=service_payload, + ) + + service_key = "simcore/services/comp/itis/sleeper" + service_version = "0.1.0" + assert client.app and client.app.router + url = ( + client.app.router["get_compatible_outputs_given_target_input"] + .url_for( + service_key=urllib.parse.quote(service_key, safe=""), + service_version=service_version, + ) + .with_query( + { + "toService": "simcore/services/comp/itis/sleeper", + "toVersion": "0.1.0", + "toInput": "input_1", + } + ) + ) + response = await client.get(f"{url}") + _, _ = await assert_status(response, status.HTTP_200_OK) + + @pytest.mark.parametrize( "user_role", [UserRole.USER], diff --git a/services/web/server/tests/unit/with_dbs/01/test_catalog_rest_client.py b/services/web/server/tests/unit/with_dbs/01/test_catalog_rest_client.py new file mode 100644 index 00000000000..386399824ec --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/01/test_catalog_rest_client.py @@ -0,0 +1,112 @@ +# pylint:disable=unused-argument +import re + +import pytest +from aiohttp.test_utils import TestClient +from aioresponses import aioresponses as AioResponsesMock +from common_library.users_enums import UserRole +from models_library.api_schemas_catalog.service_access_rights import ( + ServiceAccessRightsGet, +) +from pytest_simcore.helpers.webserver_login import UserInfoDict +from servicelib.aiohttp import status +from simcore_service_webserver.catalog._exceptions import ( + DefaultPricingUnitForServiceNotFoundError, +) +from simcore_service_webserver.catalog.catalog_service import ( + get_service_access_rights, + get_services_for_user_in_product, + is_catalog_service_responsive, +) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +@pytest.mark.parametrize( + "backend_status_code", [status.HTTP_200_OK, status.HTTP_500_INTERNAL_SERVER_ERROR] +) +async def test_server_responsive( + client: TestClient, + logged_user: UserInfoDict, + aioresponses_mocker: AioResponsesMock, + backend_status_code: int, +): + aioresponses_mocker.get("http://catalog:8000", status=backend_status_code) + + assert client.app + is_responsive = await is_catalog_service_responsive(app=client.app) + if backend_status_code == status.HTTP_200_OK: + assert is_responsive == True + else: + assert is_responsive == False + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +@pytest.mark.parametrize( + "backend_status_code", [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND] +) +async def test_get_services_for_user_in_product( + client: TestClient, + logged_user: UserInfoDict, + aioresponses_mocker: AioResponsesMock, + backend_status_code: int, +): + url_pattern = re.compile(r"http://catalog:8000/.*") + aioresponses_mocker.get( + url_pattern, + status=backend_status_code, + ) + assert client.app + _ = await get_services_for_user_in_product( + app=client.app, + user_id=logged_user["id"], + product_name="osparc", + only_key_versions=False, + ) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_get_service_access_rights( + client: TestClient, + logged_user: UserInfoDict, + aioresponses_mocker: AioResponsesMock, +): + url_pattern = re.compile(r"http://catalog:8000/.*") + example = ServiceAccessRightsGet( + service_key="simcore/services/comp/itis/sleeper", + service_version="2.1.4", + gids_with_access_rights={ + 1: {"execute_access": True}, + 5: {"execute_access": True}, + }, + ) + aioresponses_mocker.get( + url_pattern, + status=status.HTTP_200_OK, + payload=example.model_dump(), + ) + assert client.app + access_rights = await get_service_access_rights( + app=client.app, + user_id=logged_user["id"], + service_key="simcore/services/comp/itis/sleeper", + service_version="2.1.4", + product_name="osparc", + ) + assert isinstance(access_rights, ServiceAccessRightsGet) + + +async def test_catalog_exceptions(): + + error = DefaultPricingUnitForServiceNotFoundError( + service_key="key", service_version="version" + ) + assert isinstance(error.debug_message(), str) diff --git a/services/web/server/tests/unit/with_dbs/02/conftest.py b/services/web/server/tests/unit/with_dbs/02/conftest.py index 2bd29316680..22d898f2a10 100644 --- a/services/web/server/tests/unit/with_dbs/02/conftest.py +++ b/services/web/server/tests/unit/with_dbs/02/conftest.py @@ -92,12 +92,12 @@ def mock_catalog_api( ) -> dict[str, mock.Mock]: return { "get_service_resources": mocker.patch( - "simcore_service_webserver.projects.projects_service.catalog_client.get_service_resources", + "simcore_service_webserver.projects.projects_service.catalog_service.get_service_resources", return_value=mock_service_resources, autospec=True, ), "get_service": mocker.patch( - "simcore_service_webserver.projects.projects_service.catalog_client.get_service", + "simcore_service_webserver.projects.projects_service.catalog_service.get_service", return_value=mock_service, autospec=True, ), diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py index 8324aff33a2..b81d8b99dd3 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py @@ -42,7 +42,7 @@ def standard_user_role() -> tuple[str, tuple[UserRole, ExpectedResponse]]: @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__patch.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__patch.py index 04b4db5b7e8..08100ff95f5 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__patch.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__patch.py @@ -25,7 +25,7 @@ @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_handlers.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_groups_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_groups_handlers.py index 76569151068..214d284d8f0 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_groups_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_groups_handlers.py @@ -20,7 +20,7 @@ @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_handlers.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) @@ -35,15 +35,6 @@ def mock_project_uses_available_services(mocker: MockerFixture): ) -@pytest.fixture -def mock_catalog_api_get_services_for_user_in_product_2(mocker: MockerFixture): - mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", - spec=True, - return_value=[], - ) - - @pytest.mark.acceptance_test( "Driving test for https://github.com/ITISFoundation/osparc-issues/issues/1547" ) @@ -55,7 +46,6 @@ async def test_projects_groups_full_workflow( expected: HTTPStatus, mock_catalog_api_get_services_for_user_in_product, mock_project_uses_available_services, - mock_catalog_api_get_services_for_user_in_product_2, ): assert client.app # check the default project permissions diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py index 77890e530c8..1819923c9a4 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py @@ -29,7 +29,7 @@ @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_handlers.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__services_access.py b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__services_access.py index cedb64451bb..f70587ebe56 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__services_access.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__services_access.py @@ -59,7 +59,7 @@ def fake_project( @pytest.fixture def mock_catalog_api_get_service_access_rights_response(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.catalog_client.get_service_access_rights", + "simcore_service_webserver.projects._nodes_handlers.catalog_service.get_service_access_rights", spec=True, side_effect=[ ServiceAccessRightsGet( @@ -130,7 +130,7 @@ async def test_accessible_thanks_to_everyone_group_id( logged_user: UserInfoDict, ): mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.catalog_client.get_service_access_rights", + "simcore_service_webserver.projects._nodes_handlers.catalog_service.get_service_access_rights", spec=True, side_effect=[ ServiceAccessRightsGet( @@ -185,7 +185,7 @@ async def test_accessible_thanks_to_concrete_group_id( for_gid = logged_user["primary_gid"] mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.catalog_client.get_service_access_rights", + "simcore_service_webserver.projects._nodes_handlers.catalog_service.get_service_access_rights", spec=True, side_effect=[ ServiceAccessRightsGet( @@ -238,7 +238,7 @@ async def test_accessible_through_product_group( for_gid = logged_user["primary_gid"] mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.catalog_client.get_service_access_rights", + "simcore_service_webserver.projects._nodes_handlers.catalog_service.get_service_access_rights", spec=True, side_effect=[ ServiceAccessRightsGet( @@ -297,7 +297,7 @@ async def test_accessible_for_one_service( for_gid = logged_user["primary_gid"] mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.catalog_client.get_service_access_rights", + "simcore_service_webserver.projects._nodes_handlers.catalog_service.get_service_access_rights", spec=True, side_effect=[ ServiceAccessRightsGet( @@ -355,7 +355,7 @@ async def test_not_accessible_for_more_services( logged_user: UserInfoDict, ): mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.catalog_client.get_service_access_rights", + "simcore_service_webserver.projects._nodes_handlers.catalog_service.get_service_access_rights", spec=True, side_effect=[ ServiceAccessRightsGet( @@ -421,7 +421,7 @@ async def test_not_accessible_for_service_because_of_execute_access_false( for_gid = logged_user["primary_gid"] mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.catalog_client.get_service_access_rights", + "simcore_service_webserver.projects._nodes_handlers.catalog_service.get_service_access_rights", spec=True, side_effect=[ ServiceAccessRightsGet( @@ -479,7 +479,7 @@ async def test_get_project_services( ] mocker.patch( - "simcore_service_webserver.catalog._api.catalog_rpc.batch_get_my_services", + "simcore_service_webserver.catalog._service.catalog_rpc.batch_get_my_services", spec=True, return_value=[ MyServiceGet( @@ -561,7 +561,7 @@ async def test_get_project_services_service_unavailable( logged_user: UserInfoDict, ): mocker.patch( - "simcore_service_webserver.catalog._api.catalog_rpc.batch_get_my_services", + "simcore_service_webserver.catalog._service.catalog_rpc.batch_get_my_services", spec=True, side_effect=RPCServerError( exc_message="Service Unavailable", diff --git a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py index cfb2b06a789..35f6255a5e1 100644 --- a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py +++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_admin_pricing_plans.py @@ -23,7 +23,7 @@ def mock_catalog_client(mocker: MockerFixture, faker: Faker) -> dict[str, MagicMock]: return { "get_service": mocker.patch( - "simcore_service_webserver.resource_usage._pricing_plans_admin_service.catalog_client.get_service", + "simcore_service_webserver.resource_usage._pricing_plans_admin_service.catalog_service.get_service", autospec=True, ) } diff --git a/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py b/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py index f4b2df540ae..7785d0dbaeb 100644 --- a/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py +++ b/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py @@ -274,7 +274,7 @@ async def test_project_folder_movement_full_workflow( @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py index 941e4b8a098..aacee1c0887 100644 --- a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py +++ b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py @@ -283,7 +283,7 @@ def catalog_subsystem_mock(mocker: MockerFixture) -> None: ] mock = mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", autospec=True, ) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py index c301ead5f90..cb5f211f533 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py @@ -27,12 +27,7 @@ @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", - spec=True, - return_value=[], - ) - mocker.patch( - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__list_projects_full_search.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__list_projects_full_search.py index 99bbaffc4a2..279bb591665 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__list_projects_full_search.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__list_projects_full_search.py @@ -23,12 +23,7 @@ @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", - spec=True, - return_value=[], - ) - mocker.patch( - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_folders_between_workspaces.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_folders_between_workspaces.py index ea7105a3338..2f1e73ceb84 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_folders_between_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_folders_between_workspaces.py @@ -28,12 +28,7 @@ def user_role() -> UserRole: @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", - spec=True, - return_value=[], - ) - mocker.patch( - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_projects_between_workspaces.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_projects_between_workspaces.py index a81c76012a0..30cf718a37f 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_projects_between_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__moving_projects_between_workspaces.py @@ -28,12 +28,7 @@ @pytest.fixture def mock_catalog_api_get_services_for_user_in_product(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", - spec=True, - return_value=[], - ) - mocker.patch( - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", spec=True, return_value=[], ) diff --git a/services/web/server/tests/unit/with_dbs/conftest.py b/services/web/server/tests/unit/with_dbs/conftest.py index 87d80398658..10a6e91c5ad 100644 --- a/services/web/server/tests/unit/with_dbs/conftest.py +++ b/services/web/server/tests/unit/with_dbs/conftest.py @@ -1,9 +1,9 @@ -""" Configuration for unit testing with a postgress fixture +"""Configuration for unit testing with a postgress fixture - - Unit testing of webserver app with a postgress service as fixture - - Starts test session by running a postgres container as a fixture (see postgress_service) +- Unit testing of webserver app with a postgress service as fixture +- Starts test session by running a postgres container as a fixture (see postgress_service) - IMPORTANT: remember that these are still unit-tests! +IMPORTANT: remember that these are still unit-tests! """ # nopycln: file @@ -288,8 +288,7 @@ async def _mocked_get_services_for_user(*args, **kwargs): return services_in_project for namespace in ( - "simcore_service_webserver.projects._crud_api_read.get_services_for_user_in_product", - "simcore_service_webserver.projects._crud_handlers.get_services_for_user_in_product", + "simcore_service_webserver.projects._crud_api_read.catalog_service.get_services_for_user_in_product", ): mock = mocker.patch( namespace,