From 6ced45520256914bac1930d329914ca993ee9c9b Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:00:31 +0100 Subject: [PATCH 01/20] cleanup --- .../src/simcore_service_webserver/projects/_crud_api_read.py | 3 +++ 1 file changed, 3 insertions(+) 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 2abadb0982b5..5b1dda2b7622 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 @@ -17,6 +17,9 @@ from servicelib.utils import logged_gather from simcore_postgres_database.models.projects import ProjectType from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB +from simcore_service_webserver.projects._permalink_api import ( + aggregate_permalink_in_project, +) from simcore_service_webserver.projects._projects_db import ( batch_get_trashed_by_primary_gid, ) From 0d789bd07ed0f876ec4f28a1086a38e6e90835f2 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:27:42 +0100 Subject: [PATCH 02/20] rest exception handlers --- .../projects/_common/exception_handlers.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py new file mode 100644 index 000000000000..cfa5e61842fe --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py @@ -0,0 +1,67 @@ +import logging + +from servicelib.aiohttp import status + +from ...exception_handling import ( + ExceptionToHttpErrorMap, + HttpErrorInfo, + exception_handling_decorator, + to_exceptions_handlers_map, +) +from ...folders.errors import FolderAccessForbiddenError, FolderNotFoundError +from ...workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError +from ..exceptions import ( + ProjectDeleteError, + ProjectInvalidRightsError, + ProjectNotFoundError, + ProjectOwnerNotFoundInTheProjectAccessRightsError, + WrongTagIdsInQueryError, +) + +_logger = logging.getLogger(__name__) + +_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { + # + # NOTE: keep keys alphabetically sorted + # + FolderAccessForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Access to folder forbidden", + ), + FolderNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Folder not found: {reason}", + ), + ProjectDeleteError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "Failed to complete deletion of '{project_uuid}': {reason}", + ), + ProjectInvalidRightsError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Do not have sufficient access rights on project {project_uuid} for this action", + ), + ProjectNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Project not found", + ), + ProjectOwnerNotFoundInTheProjectAccessRightsError: HttpErrorInfo( + status.HTTP_400_BAD_REQUEST, + "Project owner identifier was not found in the project's access-rights field", + ), + WorkspaceAccessForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Access to workspace forbidden: {reason}", + ), + WorkspaceNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Workspace not found: {reason}", + ), + WrongTagIdsInQueryError: HttpErrorInfo( + status.HTTP_400_BAD_REQUEST, + "Wrong tag IDs in query", + ), +} + +handle_plugin_requests_exceptions = exception_handling_decorator( + to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) +) From c7df38b20c8f4b53ba324432576a7c6ead174941 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:28:06 +0100 Subject: [PATCH 03/20] uses exception handlers --- .../projects/_crud_handlers.py | 256 +++++++----------- 1 file changed, 96 insertions(+), 160 deletions(-) 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 72cbe07d9718..9ab821a95d02 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 @@ -4,7 +4,6 @@ """ -import functools import logging from aiohttp import web @@ -28,7 +27,6 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, X_SIMCORE_USER_AGENT, @@ -38,15 +36,14 @@ from .._meta import API_VTAG as VTAG from ..catalog.client import get_services_for_user_in_product -from ..folders.errors import FolderAccessForbiddenError, FolderNotFoundError 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 from ..security.api import check_user_permission from ..security.decorators import permission_required from ..users.api import get_user_fullname -from ..workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError from . import _crud_api_create, _crud_api_read, _crud_handlers_utils, projects_service +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from ._crud_handlers_models import ( ProjectActiveQueryParams, @@ -57,13 +54,6 @@ ProjectsSearchQueryParams, ) from ._permalink_service import update_or_pop_permalink_in_project -from .exceptions import ( - ProjectDeleteError, - ProjectInvalidRightsError, - ProjectNotFoundError, - ProjectOwnerNotFoundInTheProjectAccessRightsError, - WrongTagIdsInQueryError, -) from .utils import get_project_unavailable_services, project_uses_available_services # When the user requests a project with a repo, the working copy might differ from @@ -72,37 +62,9 @@ # response needs to refer to the uuid of the request and this is passed through this request key RQ_REQUESTED_REPO_PROJECT_UUID_KEY = f"{__name__}.RQT_REQUESTED_REPO_PROJECT_UUID_KEY" - _logger = logging.getLogger(__name__) -def _handle_projects_exceptions(handler: Handler): - @functools.wraps(handler) - async def _wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ( - ProjectNotFoundError, - FolderNotFoundError, - WorkspaceNotFoundError, - ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - except ( - ProjectOwnerNotFoundInTheProjectAccessRightsError, - WrongTagIdsInQueryError, - ) as exc: - raise web.HTTPBadRequest(reason=f"{exc}") from exc - except ( - ProjectInvalidRightsError, - FolderAccessForbiddenError, - WorkspaceAccessForbiddenError, - ) as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return _wrapper - - routes = web.RouteTableDef() @@ -110,7 +72,7 @@ async def _wrapper(request: web.Request) -> web.StreamResponse: @login_required @permission_required("project.create") @permission_required("services.pipeline.*") # due to update_pipeline_db -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def create_project(request: web.Request): # # - Create https://google.aip.dev/133 @@ -165,7 +127,7 @@ async def create_project(request: web.Request): @routes.get(f"/{VTAG}/projects", name="list_projects") @login_required @permission_required("project.read") -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def list_projects(request: web.Request): # # - List https://google.aip.dev/132 @@ -218,7 +180,7 @@ async def list_projects(request: web.Request): @routes.get(f"/{VTAG}/projects:search", name="list_projects_full_search") @login_required @permission_required("project.read") -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def list_projects_full_search(request: web.Request): req_ctx = RequestContext.model_validate(request) query_params: ProjectsSearchQueryParams = parse_request_query_parameters_as( @@ -258,7 +220,7 @@ async def list_projects_full_search(request: web.Request): @routes.get(f"/{VTAG}/projects/active", name="get_active_project") @login_required @permission_required("project.read") -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def get_active_project(request: web.Request) -> web.Response: # # - Get https://google.aip.dev/131 @@ -275,39 +237,35 @@ async def get_active_project(request: web.Request) -> web.Response: ProjectActiveQueryParams, request ) - try: - user_active_projects = [] - with managed_resource( - req_ctx.user_id, query_params.client_session_id, request.app - ) as rt: - # get user's projects - user_active_projects = await rt.find(PROJECT_ID_KEY) - - data = None - if user_active_projects: - project = await projects_service.get_project_for_user( - request.app, - project_uuid=user_active_projects[0], - user_id=req_ctx.user_id, - include_state=True, - include_trashed_by_primary_gid=True, - ) + user_active_projects = [] + with managed_resource( + req_ctx.user_id, query_params.client_session_id, request.app + ) as rt: + # get user's projects + user_active_projects = await rt.find(PROJECT_ID_KEY) - # updates project's permalink field - await update_or_pop_permalink_in_project(request, project) + data = None + if user_active_projects: + project = await projects_service.get_project_for_user( + request.app, + project_uuid=user_active_projects[0], + user_id=req_ctx.user_id, + include_state=True, + include_trashed_by_primary_gid=True, + ) - data = ProjectGet.from_domain_model(project).data(exclude_unset=True) + # updates project's permalink field + await update_or_pop_permalink_in_project(request, project) - return web.json_response({"data": data}, dumps=json_dumps) + data = ProjectGet.from_domain_model(project).data(exclude_unset=True) - except ProjectNotFoundError as exc: - raise web.HTTPNotFound(reason="Project not found") from exc + return web.json_response({"data": data}, dumps=json_dumps) @routes.get(f"/{VTAG}/projects/{{project_id}}", name="get_project") @login_required @permission_required("project.read") -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def get_project(request: web.Request): """ @@ -325,46 +283,36 @@ async def get_project(request: web.Request): request.app, req_ctx.user_id, req_ctx.product_name, only_key_versions=True ) - try: - project = await projects_service.get_project_for_user( - request.app, - project_uuid=f"{path_params.project_id}", - user_id=req_ctx.user_id, - include_state=True, - include_trashed_by_primary_gid=True, + project = await projects_service.get_project_for_user( + request.app, + project_uuid=f"{path_params.project_id}", + user_id=req_ctx.user_id, + include_state=True, + include_trashed_by_primary_gid=True, + ) + if not await project_uses_available_services(project, user_available_services): + unavilable_services = get_project_unavailable_services( + project, user_available_services ) - if not await project_uses_available_services(project, user_available_services): - unavilable_services = get_project_unavailable_services( - project, user_available_services - ) - formatted_services = ", ".join( - f"{service}:{version}" for service, version in unavilable_services - ) - # TODO: lack of permissions should be notified with https://httpstatuses.com/403 web.HTTPForbidden - raise web.HTTPNotFound( - reason=( - f"Project '{path_params.project_id}' uses unavailable services. Please ask " - f"for permission for the following services {formatted_services}" - ) + formatted_services = ", ".join( + f"{service}:{version}" for service, version in unavilable_services + ) + # TODO: lack of permissions should be notified with https://httpstatuses.com/403 web.HTTPForbidden + raise web.HTTPNotFound( + reason=( + f"Project '{path_params.project_id}' uses unavailable services. Please ask " + f"for permission for the following services {formatted_services}" ) + ) - if new_uuid := request.get(RQ_REQUESTED_REPO_PROJECT_UUID_KEY): - project["uuid"] = new_uuid - - # Adds permalink - await update_or_pop_permalink_in_project(request, project) + if new_uuid := request.get(RQ_REQUESTED_REPO_PROJECT_UUID_KEY): + project["uuid"] = new_uuid - data = ProjectGet.from_domain_model(project).data(exclude_unset=True) - return web.json_response({"data": data}, dumps=json_dumps) + # Adds permalink + await update_or_pop_permalink_in_project(request, project) - except ProjectInvalidRightsError as exc: - raise web.HTTPForbidden( - reason=f"You do not have sufficient rights to read project {path_params.project_id}" - ) from exc - except ProjectNotFoundError as exc: - raise web.HTTPNotFound( - reason=f"Project {path_params.project_id} not found" - ) from exc + data = ProjectGet.from_domain_model(project).data(exclude_unset=True) + return web.json_response({"data": data}, dumps=json_dumps) @routes.get( @@ -372,7 +320,7 @@ async def get_project(request: web.Request): ) @login_required @permission_required("project.read") -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def get_project_inactivity(request: web.Request): path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -386,7 +334,7 @@ async def get_project_inactivity(request: web.Request): @login_required @permission_required("project.update") @permission_required("services.pipeline.*") -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def patch_project(request: web.Request): # # Update https://google.aip.dev/134 @@ -409,7 +357,7 @@ async def patch_project(request: web.Request): @routes.delete(f"/{VTAG}/projects/{{project_id}}", name="delete_project") @login_required @permission_required("project.delete") -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def delete_project(request: web.Request): # Delete https://google.aip.dev/135 """ @@ -427,64 +375,52 @@ async def delete_project(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) - try: - await projects_service.get_project_for_user( - request.app, - project_uuid=f"{path_params.project_id}", - user_id=req_ctx.user_id, - ) - project_users: set[int] = set() - with managed_resource(req_ctx.user_id, None, request.app) as user_session: - project_users = { - s.user_id - for s in await user_session.find_users_of_resource( - request.app, PROJECT_ID_KEY, f"{path_params.project_id}" - ) - } - # that project is still in use - if req_ctx.user_id in project_users: - raise web.HTTPForbidden( - reason="Project is still open in another tab/browser." - "It cannot be deleted until it is closed." - ) - if project_users: - other_user_names = { - f"{await get_user_fullname(request.app, user_id=uid)}" - for uid in project_users - } - raise web.HTTPForbidden( - reason=f"Project is open by {other_user_names}. " - "It cannot be deleted until the project is closed." - ) - - project_locked_state: ProjectLocked | None - if project_locked_state := await get_project_locked_state( - get_redis_lock_manager_client_sdk(request.app), - project_uuid=path_params.project_id, - ): - raise web.HTTPConflict( - reason=f"Project {path_params.project_id} is locked: {project_locked_state=}" + await projects_service.get_project_for_user( + request.app, + project_uuid=f"{path_params.project_id}", + user_id=req_ctx.user_id, + ) + project_users: set[int] = set() + with managed_resource(req_ctx.user_id, None, request.app) as user_session: + project_users = { + s.user_id + for s in await user_session.find_users_of_resource( + request.app, PROJECT_ID_KEY, f"{path_params.project_id}" ) + } + # that project is still in use + if req_ctx.user_id in project_users: + raise web.HTTPForbidden( + reason="Project is still open in another tab/browser." + "It cannot be deleted until it is closed." + ) + if project_users: + other_user_names = { + f"{await get_user_fullname(request.app, user_id=uid)}" + for uid in project_users + } + raise web.HTTPForbidden( + reason=f"Project is open by {other_user_names}. " + "It cannot be deleted until the project is closed." + ) - await projects_service.submit_delete_project_task( - request.app, - project_uuid=path_params.project_id, - user_id=req_ctx.user_id, - simcore_user_agent=request.headers.get( - X_SIMCORE_USER_AGENT, UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE - ), + project_locked_state: ProjectLocked | None + if project_locked_state := await get_project_locked_state( + get_redis_lock_manager_client_sdk(request.app), + project_uuid=path_params.project_id, + ): + raise web.HTTPConflict( + reason=f"Project {path_params.project_id} is locked: {project_locked_state=}" ) - except ProjectInvalidRightsError as err: - raise web.HTTPForbidden( - reason="You do not have sufficient rights to delete this project" - ) from err - except ProjectNotFoundError as err: - raise web.HTTPNotFound( - reason=f"Project {path_params.project_id} not found" - ) from err - except ProjectDeleteError as err: - raise web.HTTPConflict(reason=f"{err}") from err + await projects_service.submit_delete_project_task( + request.app, + project_uuid=path_params.project_id, + user_id=req_ctx.user_id, + simcore_user_agent=request.headers.get( + X_SIMCORE_USER_AGENT, UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE + ), + ) return web.json_response(status=status.HTTP_204_NO_CONTENT) @@ -500,7 +436,7 @@ async def delete_project(request: web.Request): @login_required @permission_required("project.create") @permission_required("services.pipeline.*") # due to update_pipeline_db -@_handle_projects_exceptions +@handle_plugin_requests_exceptions async def clone_project(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) From 0017c7ff53d7a9004f790e7b05adf2d9bf7f453f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:23:56 +0100 Subject: [PATCH 04/20] cleanup after merge --- .../simcore_service_webserver/projects/_crud_api_read.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 5b1dda2b7622..24a51ea4142c 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 @@ -5,7 +5,8 @@ """ -from typing import Any, Coroutine +from collections.abc import Coroutine +from typing import Any from aiohttp import web from models_library.folders import FolderID, FolderQuery, FolderScope @@ -17,15 +18,12 @@ from servicelib.utils import logged_gather from simcore_postgres_database.models.projects import ProjectType from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB -from simcore_service_webserver.projects._permalink_api import ( - aggregate_permalink_in_project, -) from simcore_service_webserver.projects._projects_db import ( batch_get_trashed_by_primary_gid, ) from ..catalog.client import get_services_for_user_in_product -from ..folders import _folders_repository as _folders_repository +from ..folders import _folders_repository from ..workspaces._workspaces_service import check_user_workspace_access from . import projects_service from .db import ProjectDBAPI From c2d03c9866e7cbdf770a1e1cc51220aa74f3782b Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:29:33 +0100 Subject: [PATCH 05/20] foldres and groups excpeiotns --- .../projects/_common/exception_handlers.py | 7 ++++- .../projects/_folders_handlers.py | 21 ++------------- .../projects/_groups_handlers.py | 27 ++++--------------- 3 files changed, 13 insertions(+), 42 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py index cfa5e61842fe..36026c6814ee 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py @@ -12,6 +12,7 @@ from ...workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError from ..exceptions import ( ProjectDeleteError, + ProjectGroupNotFoundError, ProjectInvalidRightsError, ProjectNotFoundError, ProjectOwnerNotFoundInTheProjectAccessRightsError, @@ -36,13 +37,17 @@ status.HTTP_409_CONFLICT, "Failed to complete deletion of '{project_uuid}': {reason}", ), + ProjectGroupNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Project group not found: {reason}", + ), ProjectInvalidRightsError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, "Do not have sufficient access rights on project {project_uuid} for this action", ), ProjectNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Project not found", + "Project {project_uuid} not found", ), ProjectOwnerNotFoundInTheProjectAccessRightsError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, diff --git a/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py index c4f1828237b8..3ca23874c939 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py @@ -1,4 +1,3 @@ -import functools import logging from aiohttp import web @@ -8,33 +7,17 @@ from pydantic import BaseModel, ConfigDict, field_validator from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as -from servicelib.aiohttp.typing_extension import Handler from .._meta import api_version_prefix as VTAG from ..login.decorators import login_required from ..security.decorators import permission_required from . import _folders_api +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import RequestContext -from .exceptions import ProjectGroupNotFoundError, ProjectNotFoundError _logger = logging.getLogger(__name__) -def _handle_projects_folders_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ProjectGroupNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except ProjectNotFoundError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper - - routes = web.RouteTableDef() @@ -55,7 +38,7 @@ class _ProjectsFoldersPathParams(BaseModel): ) @login_required @permission_required("project.folders.*") -@_handle_projects_folders_exceptions +@handle_plugin_requests_exceptions async def replace_project_folder(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsFoldersPathParams, request) diff --git a/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py index d507a2b1eff9..27fa40d2e0b8 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py @@ -2,7 +2,6 @@ """ -import functools import logging from aiohttp import web @@ -14,35 +13,19 @@ parse_request_body_as, parse_request_path_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler from .._meta import api_version_prefix as VTAG from ..login.decorators import login_required from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _groups_api +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from ._groups_api import ProjectGroupGet -from .exceptions import ProjectGroupNotFoundError, ProjectNotFoundError _logger = logging.getLogger(__name__) -def _handle_projects_groups_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ProjectGroupNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except ProjectNotFoundError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper - - # # projects groups COLLECTION ------------------------- # @@ -68,7 +51,7 @@ class _ProjectsGroupsBodyParams(BaseModel): ) @login_required @permission_required("project.access_rights.update") -@_handle_projects_groups_exceptions +@handle_plugin_requests_exceptions async def create_project_group(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsGroupsPathParams, request) @@ -91,7 +74,7 @@ async def create_project_group(request: web.Request): @routes.get(f"/{VTAG}/projects/{{project_id}}/groups", name="list_project_groups") @login_required @permission_required("project.read") -@_handle_projects_groups_exceptions +@handle_plugin_requests_exceptions async def list_project_groups(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -114,7 +97,7 @@ async def list_project_groups(request: web.Request): ) @login_required @permission_required("project.access_rights.update") -@_handle_projects_groups_exceptions +@handle_plugin_requests_exceptions async def replace_project_group(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsGroupsPathParams, request) @@ -138,7 +121,7 @@ async def replace_project_group(request: web.Request): ) @login_required @permission_required("project.access_rights.update") -@_handle_projects_groups_exceptions +@handle_plugin_requests_exceptions async def delete_project_group(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectsGroupsPathParams, request) From b5c1229cfd06ddcb43299933f8dce8e7515a779e Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:44:24 +0100 Subject: [PATCH 06/20] nodes --- .../projects/_common/exception_handlers.py | 65 ++++++++++++++++++- .../projects/_metadata_handlers.py | 36 +--------- 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py index 36026c6814ee..f9151fbf18e7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py @@ -1,6 +1,7 @@ import logging from servicelib.aiohttp import status +from servicelib.rabbitmq.rpc_interfaces.catalog.errors import CatalogForbiddenError from ...exception_handling import ( ExceptionToHttpErrorMap, @@ -9,13 +10,24 @@ to_exceptions_handlers_map, ) from ...folders.errors import FolderAccessForbiddenError, FolderNotFoundError +from ...resource_usage.errors import DefaultPricingPlanNotFoundError +from ...users.exceptions import UserDefaultWalletNotFoundError +from ...wallets.errors import WalletAccessForbiddenError, WalletNotEnoughCreditsError from ...workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError from ..exceptions import ( + ClustersKeeperNotAvailableError, + DefaultPricingUnitNotFoundError, + NodeNotFoundError, + ParentNodeNotFoundError, ProjectDeleteError, ProjectGroupNotFoundError, + ProjectInDebtCanNotChangeWalletError, ProjectInvalidRightsError, + ProjectInvalidUsageError, + ProjectNodeRequiredInputsNotSetError, ProjectNotFoundError, ProjectOwnerNotFoundInTheProjectAccessRightsError, + ProjectStartsTooManyDynamicNodesError, WrongTagIdsInQueryError, ) @@ -33,6 +45,14 @@ status.HTTP_404_NOT_FOUND, "Folder not found: {reason}", ), + NodeNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Node '{node_uuid}' not found in project '{project_uuid}'", + ), + ParentNodeNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Parent node '{node_uuid}' not found", + ), ProjectDeleteError: HttpErrorInfo( status.HTTP_409_CONFLICT, "Failed to complete deletion of '{project_uuid}': {reason}", @@ -45,6 +65,10 @@ status.HTTP_403_FORBIDDEN, "Do not have sufficient access rights on project {project_uuid} for this action", ), + ProjectInvalidUsageError: HttpErrorInfo( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "Invalid usage for project", + ), ProjectNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, "Project {project_uuid} not found", @@ -65,8 +89,47 @@ status.HTTP_400_BAD_REQUEST, "Wrong tag IDs in query", ), + UserDefaultWalletNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "User default wallet not found", + ), + DefaultPricingPlanNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Default pricing plan not found", + ), + DefaultPricingUnitNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Default pricing unit not found", + ), + WalletNotEnoughCreditsError: HttpErrorInfo( + status.HTTP_402_PAYMENT_REQUIRED, + "Wallet does not have enough credits. {reason}", + ), + ProjectInDebtCanNotChangeWalletError: HttpErrorInfo( + status.HTTP_402_PAYMENT_REQUIRED, + "Unable to change the credit account linked to the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + ), + ProjectStartsTooManyDynamicNodesError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "The maximal amount of concurrently running dynamic services was reached. Please manually stop a service and retry.", + ), + ClustersKeeperNotAvailableError: HttpErrorInfo( + status.HTTP_503_SERVICE_UNAVAILABLE, + "Clusters-keeper service is not available", + ), + ProjectNodeRequiredInputsNotSetError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "Project node is required but input is not set", + ), + CatalogForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Catalog forbidden: Insufficient access rights for {name}", + ), + WalletAccessForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Payment required, but the user lacks access to the project's linked wallet: Wallet access forbidden. {reason}", + ), } - handle_plugin_requests_exceptions = exception_handling_decorator( to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py index df139c6fd30b..6f20a9eb4a85 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py @@ -10,7 +10,6 @@ - Get and Update methods only """ -import functools import logging from aiohttp import web @@ -22,7 +21,6 @@ parse_request_body_as, parse_request_path_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler from servicelib.logging_utils import log_catch from .._meta import api_version_prefix @@ -30,42 +28,14 @@ from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _metadata_api +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext -from .exceptions import ( - NodeNotFoundError, - ParentNodeNotFoundError, - ProjectInvalidRightsError, - ProjectInvalidUsageError, - ProjectNotFoundError, -) routes = web.RouteTableDef() _logger = logging.getLogger(__name__) -def _handle_project_exceptions(handler: Handler): - """Transforms project errors -> http errors""" - - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ( - ProjectNotFoundError, - NodeNotFoundError, - ParentNodeNotFoundError, - ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - except ProjectInvalidRightsError as exc: - raise web.HTTPUnauthorized(reason=f"{exc}") from exc - except ProjectInvalidUsageError as exc: - raise web.HTTPUnprocessableEntity(reason=f"{exc}") from exc - - return wrapper - - # # projects/*/custom-metadata # @@ -77,7 +47,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: ) @login_required @permission_required("project.read") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def get_project_metadata(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -97,7 +67,7 @@ async def get_project_metadata(request: web.Request) -> web.Response: ) @login_required @permission_required("project.update") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def update_project_metadata(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) From c43a0b8182e717a5b208827b30ee30d480697723 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:46:08 +0100 Subject: [PATCH 07/20] same response --- .../src/simcore_service_webserver/projects/_crud_handlers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 9ab821a95d02..26871835482d 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 @@ -33,6 +33,7 @@ ) from servicelib.redis import get_project_locked_state from simcore_service_webserver.projects.models import ProjectDict +from simcore_service_webserver.utils_aiohttp import envelope_json_response from .._meta import API_VTAG as VTAG from ..catalog.client import get_services_for_user_in_product @@ -259,7 +260,7 @@ async def get_active_project(request: web.Request) -> web.Response: data = ProjectGet.from_domain_model(project).data(exclude_unset=True) - return web.json_response({"data": data}, dumps=json_dumps) + return envelope_json_response(data) @routes.get(f"/{VTAG}/projects/{{project_id}}", name="get_project") @@ -312,7 +313,7 @@ async def get_project(request: web.Request): await update_or_pop_permalink_in_project(request, project) data = ProjectGet.from_domain_model(project).data(exclude_unset=True) - return web.json_response({"data": data}, dumps=json_dumps) + return envelope_json_response(data) @routes.get( From d92be652591a7145447d6b49b48cfe82e48a51aa Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:49:40 +0100 Subject: [PATCH 08/20] error handling in ports_rest --- .../projects/_nodes_handlers.py | 84 ++++--------------- .../projects/_ports_handlers.py | 67 ++++----------- 2 files changed, 29 insertions(+), 122 deletions(-) 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 44ac05a12c72..1a3dd3d07b7b 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 @@ -3,7 +3,6 @@ """ import asyncio -import functools import logging from aiohttp import web @@ -42,17 +41,12 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, X_SIMCORE_USER_AGENT, ) from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.rabbitmq import RPCServerError -from servicelib.rabbitmq.rpc_interfaces.catalog.errors import ( - CatalogForbiddenError, - CatalogItemNotFoundError, -) from servicelib.rabbitmq.rpc_interfaces.dynamic_scheduler.errors import ( ServiceWaitingForManualInterventionError, ServiceWasNotFoundError, @@ -67,70 +61,22 @@ from ..groups.exceptions import GroupNotFoundError from ..login.decorators import login_required from ..projects.api import has_user_project_access_rights -from ..resource_usage.errors import DefaultPricingPlanNotFoundError from ..security.decorators import permission_required from ..users.api import get_user_id_from_gid, get_user_role -from ..users.exceptions import UserDefaultWalletNotFoundError from ..utils_aiohttp import envelope_json_response -from ..wallets.errors import WalletAccessForbiddenError, WalletNotEnoughCreditsError from . import nodes_utils, projects_service +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from ._nodes_api import NodeScreenshot, get_node_screenshots from .exceptions import ( - ClustersKeeperNotAvailableError, - DefaultPricingUnitNotFoundError, NodeNotFoundError, - ProjectInDebtCanNotChangeWalletError, - ProjectInvalidRightsError, - ProjectNodeRequiredInputsNotSetError, ProjectNodeResourcesInsufficientRightsError, ProjectNodeResourcesInvalidError, - ProjectNotFoundError, - ProjectStartsTooManyDynamicNodesError, ) _logger = logging.getLogger(__name__) -def _handle_project_nodes_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ( - ProjectNotFoundError, - NodeNotFoundError, - UserDefaultWalletNotFoundError, - DefaultPricingPlanNotFoundError, - DefaultPricingUnitNotFoundError, - GroupNotFoundError, - CatalogItemNotFoundError, - ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - except ( - WalletNotEnoughCreditsError, - ProjectInDebtCanNotChangeWalletError, - ) as exc: - raise web.HTTPPaymentRequired(reason=f"{exc}") from exc - except ProjectInvalidRightsError as exc: - raise web.HTTPUnauthorized(reason=f"{exc}") from exc - except ProjectStartsTooManyDynamicNodesError as exc: - raise web.HTTPConflict(reason=f"{exc}") from exc - except ClustersKeeperNotAvailableError as exc: - raise web.HTTPServiceUnavailable(reason=f"{exc}") from exc - except ProjectNodeRequiredInputsNotSetError as exc: - raise web.HTTPConflict(reason=f"{exc}") from exc - except CatalogForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - except WalletAccessForbiddenError as exc: - raise web.HTTPForbidden( - reason=f"Payment required, but the user lacks access to the project's linked wallet.: {exc}" - ) from exc - - return wrapper - - # # projects/*/nodes COLLECTION ------------------------- # @@ -145,7 +91,7 @@ class NodePathParams(ProjectPathParams): @routes.post(f"/{VTAG}/projects/{{project_id}}/nodes", name="create_node") @login_required @permission_required("project.node.create") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def create_node(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -187,7 +133,7 @@ async def create_node(request: web.Request) -> web.Response: @routes.get(f"/{VTAG}/projects/{{project_id}}/nodes/{{node_id}}", name="get_node") @login_required @permission_required("project.node.read") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions # NOTE: Careful, this endpoint is actually "get_node_state," and it doesn't return a Node resource. async def get_node(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) @@ -226,7 +172,7 @@ async def get_node(request: web.Request) -> web.Response: ) @login_required @permission_required("project.node.update") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def patch_project_node(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) @@ -247,7 +193,7 @@ async def patch_project_node(request: web.Request) -> web.Response: @routes.delete(f"/{VTAG}/projects/{{project_id}}/nodes/{{node_id}}", name="delete_node") @login_required @permission_required("project.node.delete") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def delete_node(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) @@ -275,7 +221,7 @@ async def delete_node(request: web.Request) -> web.Response: ) @login_required @permission_required("project.node.read") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def retrieve_node(request: web.Request) -> web.Response: """Has only effect on nodes associated to dynamic services""" path_params = parse_request_path_parameters_as(NodePathParams, request) @@ -295,7 +241,7 @@ async def retrieve_node(request: web.Request) -> web.Response: ) @login_required @permission_required("project.node.update") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def update_node_outputs(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) @@ -322,7 +268,7 @@ async def update_node_outputs(request: web.Request) -> web.Response: ) @login_required @permission_required("project.update") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def start_node(request: web.Request) -> web.Response: """Has only effect on nodes associated to dynamic services""" req_ctx = RequestContext.model_validate(request) @@ -366,7 +312,7 @@ async def _stop_dynamic_service_task( ) @login_required @permission_required("project.update") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def stop_node(request: web.Request) -> web.Response: """Has only effect on nodes associated to dynamic services""" req_ctx = RequestContext.model_validate(request) @@ -408,7 +354,7 @@ async def stop_node(request: web.Request) -> web.Response: ) @login_required @permission_required("project.node.read") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def restart_node(request: web.Request) -> web.Response: """Has only effect on nodes associated to dynamic services""" @@ -432,7 +378,7 @@ async def restart_node(request: web.Request) -> web.Response: ) @login_required @permission_required("project.node.read") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def get_node_resources(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) @@ -465,7 +411,7 @@ async def get_node_resources(request: web.Request) -> web.Response: ) @login_required @permission_required("project.node.update") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def replace_node_resources(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) @@ -524,7 +470,7 @@ class _ProjectGroupAccess(BaseModel): ) @login_required @permission_required("project.read") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def get_project_services_access_for_gid( request: web.Request, ) -> web.Response: @@ -644,7 +590,7 @@ class _ProjectNodePreview(BaseModel): ) @login_required @permission_required("project.read") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def list_project_nodes_previews(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -684,7 +630,7 @@ async def list_project_nodes_previews(request: web.Request) -> web.Response: ) @login_required @permission_required("project.read") -@_handle_project_nodes_exceptions +@handle_plugin_requests_exceptions async def get_project_node_preview(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) diff --git a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py index b134929a8afb..df44d5865917 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py @@ -3,13 +3,10 @@ - /projects/{*}/outputs """ -import functools import logging -from collections.abc import Awaitable, Callable from typing import Any, Literal from aiohttp import web -from common_library.json_serialization import json_dumps from models_library.api_schemas_webserver.projects_ports import ( ProjectInputGet, ProjectInputUpdate, @@ -27,57 +24,21 @@ parse_request_body_as, parse_request_path_parameters_as, ) +from simcore_service_webserver.utils_aiohttp import envelope_json_response from .._meta import API_VTAG as VTAG from ..login.decorators import login_required from ..projects._access_rights_api import check_user_project_permission from ..security.decorators import permission_required from . import _ports_api, projects_service +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from .db import ProjectDBAPI -from .exceptions import ( - NodeNotFoundError, - ProjectInvalidRightsError, - ProjectNotFoundError, -) from .models import ProjectDict log = logging.getLogger(__name__) -def _web_json_response_enveloped(data: Any) -> web.Response: - return web.json_response( - { - "data": jsonable_encoder(data), - }, - dumps=json_dumps, - ) - - -def _handle_project_exceptions( - handler: Callable[[web.Request], Awaitable[web.Response]] -) -> Callable[[web.Request], Awaitable[web.Response]]: - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.Response: - try: - return await handler(request) - - except ProjectNotFoundError as exc: - raise web.HTTPNotFound( - reason=f"Project '{exc.project_uuid}' not found" - ) from exc - - except ProjectInvalidRightsError as exc: - raise web.HTTPUnauthorized from exc - - except NodeNotFoundError as exc: - raise web.HTTPNotFound( - reason=f"Port '{exc.node_uuid}' not found in node '{exc.project_uuid}'" - ) from exc - - return wrapper - - async def _get_validated_workbench_model( app: web.Application, project_id: ProjectID, user_id: UserID ) -> dict[NodeID, Node]: @@ -101,7 +62,7 @@ async def _get_validated_workbench_model( @routes.get(f"/{VTAG}/projects/{{project_id}}/inputs", name="get_project_inputs") @login_required @permission_required("project.read") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def get_project_inputs(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -113,8 +74,8 @@ async def get_project_inputs(request: web.Request) -> web.Response: ) inputs: dict[NodeID, Any] = _ports_api.get_project_inputs(workbench) - return _web_json_response_enveloped( - data={ + return envelope_json_response( + { node_id: ProjectInputGet( key=node_id, label=workbench[node_id].label, value=value ) @@ -126,7 +87,7 @@ async def get_project_inputs(request: web.Request) -> web.Response: @routes.patch(f"/{VTAG}/projects/{{project_id}}/inputs", name="update_project_inputs") @login_required @permission_required("project.update") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def update_project_inputs(request: web.Request) -> web.Response: db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) req_ctx = RequestContext.model_validate(request) @@ -174,8 +135,8 @@ async def update_project_inputs(request: web.Request) -> web.Response: ) inputs: dict[NodeID, Any] = _ports_api.get_project_inputs(workbench) - return _web_json_response_enveloped( - data={ + return envelope_json_response( + { node_id: ProjectInputGet( key=node_id, label=workbench[node_id].label, value=value ) @@ -192,7 +153,7 @@ async def update_project_inputs(request: web.Request) -> web.Response: @routes.get(f"/{VTAG}/projects/{{project_id}}/outputs", name="get_project_outputs") @login_required @permission_required("project.read") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def get_project_outputs(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -206,8 +167,8 @@ async def get_project_outputs(request: web.Request) -> web.Response: request.app, project_id=path_params.project_id, workbench=workbench ) - return _web_json_response_enveloped( - data={ + return envelope_json_response( + { node_id: ProjectOutputGet( key=node_id, label=workbench[node_id].label, value=value ) @@ -239,7 +200,7 @@ class ProjectMetadataPortGet(BaseModel): ) @login_required @permission_required("project.read") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def list_project_metadata_ports(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -250,8 +211,8 @@ async def list_project_metadata_ports(request: web.Request) -> web.Response: app=request.app, project_id=path_params.project_id, user_id=req_ctx.user_id ) - return _web_json_response_enveloped( - data=[ + return envelope_json_response( + [ ProjectMetadataPortGet( key=port.node_id, kind=port.kind, From c94ba31267cf0617fa196707e67997a169f6f8f0 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:53:53 +0100 Subject: [PATCH 09/20] error handling in states rest --- .../projects/_common/exception_handlers.py | 10 ++++ .../projects/_states_handlers.py | 53 ++----------------- 2 files changed, 15 insertions(+), 48 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py index f9151fbf18e7..cc61a531870b 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py @@ -22,12 +22,14 @@ ProjectDeleteError, ProjectGroupNotFoundError, ProjectInDebtCanNotChangeWalletError, + ProjectInDebtCanNotOpenError, ProjectInvalidRightsError, ProjectInvalidUsageError, ProjectNodeRequiredInputsNotSetError, ProjectNotFoundError, ProjectOwnerNotFoundInTheProjectAccessRightsError, ProjectStartsTooManyDynamicNodesError, + ProjectTooManyProjectOpenedError, WrongTagIdsInQueryError, ) @@ -77,6 +79,10 @@ status.HTTP_400_BAD_REQUEST, "Project owner identifier was not found in the project's access-rights field", ), + ProjectTooManyProjectOpenedError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "You cannot open more than {max_num_projects} study/ies at once. Please close another study and retry.", + ), WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, "Access to workspace forbidden: {reason}", @@ -109,6 +115,10 @@ status.HTTP_402_PAYMENT_REQUIRED, "Unable to change the credit account linked to the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", ), + ProjectInDebtCanNotOpenError: HttpErrorInfo( + status.HTTP_402_PAYMENT_REQUIRED, + "Unable to open the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + ), ProjectStartsTooManyDynamicNodesError: HttpErrorInfo( status.HTTP_409_CONFLICT, "The maximal amount of concurrently running dynamic services was reached. Please manually stop a service and retry.", diff --git a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py index 956226d7f32d..082fb1135799 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py @@ -1,7 +1,6 @@ """handlers for project states""" import contextlib -import functools import json import logging @@ -14,7 +13,6 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler from servicelib.aiohttp.web_exceptions_extension import HTTPLockedError from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, @@ -28,24 +26,14 @@ from ..login.decorators import login_required from ..notifications import project_logs from ..products.api import Product, get_current_product -from ..resource_usage.errors import DefaultPricingPlanNotFoundError from ..security.decorators import permission_required from ..users import api -from ..users.exceptions import UserDefaultWalletNotFoundError from ..utils_aiohttp import envelope_json_response -from ..wallets.errors import WalletNotEnoughCreditsError from . import api as projects_api from . import projects_service +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext -from .exceptions import ( - DefaultPricingUnitNotFoundError, - ProjectInDebtCanNotChangeWalletError, - ProjectInDebtCanNotOpenError, - ProjectInvalidRightsError, - ProjectNotFoundError, - ProjectStartsTooManyDynamicNodesError, - ProjectTooManyProjectOpenedError, -) +from .exceptions import ProjectStartsTooManyDynamicNodesError _logger = logging.getLogger(__name__) @@ -53,38 +41,6 @@ routes = web.RouteTableDef() -def _handle_project_exceptions(handler: Handler): - """Transforms common project errors -> http errors""" - - @functools.wraps(handler) - async def _wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ( - ProjectNotFoundError, - UserDefaultWalletNotFoundError, - DefaultPricingPlanNotFoundError, - DefaultPricingUnitNotFoundError, - ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except ProjectInvalidRightsError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - except ProjectTooManyProjectOpenedError as exc: - raise web.HTTPConflict(reason=f"{exc}") from exc - - except ( - WalletNotEnoughCreditsError, - ProjectInDebtCanNotChangeWalletError, - ProjectInDebtCanNotOpenError, - ) as exc: - raise web.HTTPPaymentRequired(reason=f"{exc}") from exc - - return _wrapper - - # # open project: custom methods https://google.aip.dev/136 # @@ -97,7 +53,7 @@ class _OpenProjectQuery(BaseModel): @routes.post(f"/{VTAG}/projects/{{project_id}}:open", name="open_project") @login_required @permission_required("project.open") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def open_project(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -208,7 +164,7 @@ async def open_project(request: web.Request) -> web.Response: @routes.post(f"/{VTAG}/projects/{{project_id}}:close", name="close_project") @login_required @permission_required("project.close") -@_handle_project_exceptions +@handle_plugin_requests_exceptions async def close_project(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -247,6 +203,7 @@ async def close_project(request: web.Request) -> web.Response: @routes.get(f"/{VTAG}/projects/{{project_id}}/state", name="get_project_state") @login_required @permission_required("project.read") +@handle_plugin_requests_exceptions async def get_project_state(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) From 184b70d70f023613dee76cf21f9430149b07edbf Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:05:27 +0100 Subject: [PATCH 10/20] errors in tags, trash, wallets and workspaces --- .../common-library/tests/test_iter_tools.py | 0 .../projects/_common/exception_handlers.py | 7 +++- .../projects/_tags_handlers.py | 3 ++ .../projects/_trash_rest.py | 11 +++-- .../projects/_wallets_handlers.py | 41 ++----------------- .../projects/_workspaces_handlers.py | 31 +------------- 6 files changed, 22 insertions(+), 71 deletions(-) create mode 100644 packages/common-library/tests/test_iter_tools.py diff --git a/packages/common-library/tests/test_iter_tools.py b/packages/common-library/tests/test_iter_tools.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py index cc61a531870b..bd3e7bb20d86 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py @@ -30,6 +30,7 @@ ProjectOwnerNotFoundInTheProjectAccessRightsError, ProjectStartsTooManyDynamicNodesError, ProjectTooManyProjectOpenedError, + ProjectWalletPendingTransactionError, WrongTagIdsInQueryError, ) @@ -97,7 +98,7 @@ ), UserDefaultWalletNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "User default wallet not found", + "Wallet not found: {reason}", ), DefaultPricingPlanNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, @@ -123,6 +124,10 @@ status.HTTP_409_CONFLICT, "The maximal amount of concurrently running dynamic services was reached. Please manually stop a service and retry.", ), + ProjectWalletPendingTransactionError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "Project has currently pending transactions. It is forbidden to change wallet.", + ), ClustersKeeperNotAvailableError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, "Clusters-keeper service is not available", diff --git a/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py index 1eff696a1773..d57bbe49756a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py @@ -13,6 +13,7 @@ from ..login.decorators import login_required from ..security.decorators import permission_required from . import _tags_api as tags_api +from ._common.exception_handlers import handle_plugin_requests_exceptions _logger = logging.getLogger(__name__) @@ -25,6 +26,7 @@ ) @login_required @permission_required("project.tag.*") +@handle_plugin_requests_exceptions async def add_project_tag(request: web.Request): user_id: int = request[RQT_USERID_KEY] @@ -51,6 +53,7 @@ async def add_project_tag(request: web.Request): ) @login_required @permission_required("project.tag.*") +@handle_plugin_requests_exceptions async def remove_project_tag(request: web.Request): user_id: int = request[RQT_USERID_KEY] diff --git a/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py b/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py index 22368285efc6..968da60e5365 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py @@ -18,13 +18,14 @@ from ..products.api import get_product_name from ..security.decorators import permission_required from . import _trash_service +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RemoveQueryParams from .exceptions import ProjectRunningConflictError, ProjectStoppingError _logger = logging.getLogger(__name__) # -# EXCEPTIONS HANDLING +# LOCAL EXCEPTIONS HANDLING # @@ -40,7 +41,7 @@ } -_handle_exceptions = exception_handling_decorator( +_handle_local_request_exceptions = exception_handling_decorator( to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) ) @@ -55,7 +56,8 @@ @routes.post(f"/{VTAG}/projects/{{project_id}}:trash", name="trash_project") @login_required @permission_required("project.delete") -@_handle_exceptions +@handle_plugin_requests_exceptions +@_handle_local_request_exceptions async def trash_project(request: web.Request): user_id = get_user_id(request) product_name = get_product_name(request) @@ -79,7 +81,8 @@ async def trash_project(request: web.Request): @routes.post(f"/{VTAG}/projects/{{project_id}}:untrash", name="untrash_project") @login_required @permission_required("project.delete") -@_handle_exceptions +@handle_plugin_requests_exceptions +@_handle_local_request_exceptions async def untrash_project(request: web.Request): user_id = get_user_id(request) product_name = get_product_name(request) diff --git a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py index 346958c34879..deef8c6d97b4 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py @@ -2,7 +2,6 @@ """ -import functools import logging from decimal import Decimal from typing import Annotated @@ -17,58 +16,26 @@ parse_request_body_as, parse_request_path_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler from simcore_service_webserver.utils_aiohttp import envelope_json_response from .._meta import API_VTAG from ..login.decorators import login_required from ..security.decorators import permission_required -from ..wallets.errors import WalletAccessForbiddenError, WalletNotFoundError from . import _wallets_api as wallets_api from . import projects_service +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext -from .exceptions import ( - ProjectInDebtCanNotChangeWalletError, - ProjectInvalidRightsError, - ProjectNotFoundError, - ProjectWalletPendingTransactionError, -) _logger = logging.getLogger(__name__) -def _handle_project_wallet_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ProjectNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except WalletNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except ProjectInDebtCanNotChangeWalletError as exc: - raise web.HTTPPaymentRequired(reason=f"{exc}") from exc - - except ( - WalletAccessForbiddenError, - ProjectInvalidRightsError, - ProjectWalletPendingTransactionError, - ) as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper - - routes = web.RouteTableDef() @routes.get(f"/{API_VTAG}/projects/{{project_id}}/wallet", name="get_project_wallet") @login_required @permission_required("project.wallet.*") -@_handle_project_wallet_exceptions +@handle_plugin_requests_exceptions async def get_project_wallet(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) @@ -99,7 +66,7 @@ class _ProjectWalletPathParams(BaseModel): ) @login_required @permission_required("project.wallet.*") -@_handle_project_wallet_exceptions +@handle_plugin_requests_exceptions async def connect_wallet_to_project(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectWalletPathParams, request) @@ -134,7 +101,7 @@ class _PayProjectDebtBody(BaseModel): ) @login_required @permission_required("project.wallet.*") -@_handle_project_wallet_exceptions +@handle_plugin_requests_exceptions async def pay_project_debt(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectWalletPathParams, request) diff --git a/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py index b5a6082cb506..aa2ea6ced897 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py @@ -1,4 +1,3 @@ -import functools import logging from typing import Annotated @@ -9,43 +8,17 @@ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as -from servicelib.aiohttp.typing_extension import Handler from .._meta import api_version_prefix as VTAG -from ..folders.errors import FolderAccessForbiddenError, FolderNotFoundError from ..login.decorators import login_required from ..security.decorators import permission_required -from ..workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError from . import _workspaces_api +from ._common.exception_handlers import handle_plugin_requests_exceptions from ._common.models import RequestContext -from .exceptions import ProjectInvalidRightsError, ProjectNotFoundError _logger = logging.getLogger(__name__) -def _handle_projects_workspaces_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ( - ProjectNotFoundError, - FolderNotFoundError, - WorkspaceNotFoundError, - ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except ( - ProjectInvalidRightsError, - FolderAccessForbiddenError, - WorkspaceAccessForbiddenError, - ) as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper - - routes = web.RouteTableDef() @@ -64,7 +37,7 @@ class _ProjectWorkspacesPathParams(BaseModel): ) @login_required @permission_required("project.workspaces.*") -@_handle_projects_workspaces_exceptions +@handle_plugin_requests_exceptions async def move_project_to_workspace(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( From 56edff043caeb94ad5445167e374a28eacb43ae4 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:20:32 +0100 Subject: [PATCH 11/20] del --- packages/common-library/tests/test_iter_tools.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/common-library/tests/test_iter_tools.py diff --git a/packages/common-library/tests/test_iter_tools.py b/packages/common-library/tests/test_iter_tools.py deleted file mode 100644 index e69de29bb2d1..000000000000 From 3c984204af233543e4c3de673816ec32a588edbd Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:26:43 +0100 Subject: [PATCH 12/20] rename module --- api/specs/web-server/_projects.py | 7 +++++++ .../{exception_handlers.py => exceptions_handlers.py} | 0 .../simcore_service_webserver/projects/_crud_handlers.py | 2 +- .../projects/_folders_handlers.py | 2 +- .../simcore_service_webserver/projects/_groups_handlers.py | 2 +- .../projects/_metadata_handlers.py | 2 +- .../simcore_service_webserver/projects/_nodes_handlers.py | 2 +- .../simcore_service_webserver/projects/_ports_handlers.py | 2 +- .../simcore_service_webserver/projects/_states_handlers.py | 2 +- .../simcore_service_webserver/projects/_tags_handlers.py | 2 +- .../src/simcore_service_webserver/projects/_trash_rest.py | 2 +- .../projects/_wallets_handlers.py | 2 +- .../projects/_workspaces_handlers.py | 2 +- 13 files changed, 18 insertions(+), 11 deletions(-) rename services/web/server/src/simcore_service_webserver/projects/_common/{exception_handlers.py => exceptions_handlers.py} (100%) diff --git a/api/specs/web-server/_projects.py b/api/specs/web-server/_projects.py index 315739d0dfcd..4c5a96c31989 100644 --- a/api/specs/web-server/_projects.py +++ b/api/specs/web-server/_projects.py @@ -27,9 +27,13 @@ from models_library.generics import Envelope from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID +from models_library.rest_error import EnvelopedError from models_library.rest_pagination import Page from pydantic import BaseModel from simcore_service_webserver._meta import API_VTAG +from simcore_service_webserver.projects._common.exceptions_handlers import ( + _TO_HTTP_ERROR_MAP, +) from simcore_service_webserver.projects._common.models import ProjectPathParams from simcore_service_webserver.projects._crud_handlers import ProjectCreateQueryParams from simcore_service_webserver.projects._crud_handlers_models import ( @@ -43,6 +47,9 @@ tags=[ "projects", ], + responses={ + i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values() + }, ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/projects/_common/exception_handlers.py rename to services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py 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 26871835482d..f121be0a0eea 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 @@ -44,7 +44,7 @@ from ..security.decorators import permission_required from ..users.api import get_user_fullname from . import _crud_api_create, _crud_api_read, _crud_handlers_utils, projects_service -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from ._crud_handlers_models import ( ProjectActiveQueryParams, diff --git a/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py index 3ca23874c939..7a213d0a1442 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_folders_handlers.py @@ -12,7 +12,7 @@ from ..login.decorators import login_required from ..security.decorators import permission_required from . import _folders_api -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import RequestContext _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py index 27fa40d2e0b8..20128d6852f6 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_groups_handlers.py @@ -19,7 +19,7 @@ from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _groups_api -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from ._groups_api import ProjectGroupGet diff --git a/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py index 6f20a9eb4a85..c6c1157e34ee 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_metadata_handlers.py @@ -28,7 +28,7 @@ from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _metadata_api -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext routes = web.RouteTableDef() 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 1a3dd3d07b7b..28fcb974511b 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 @@ -65,7 +65,7 @@ from ..users.api import get_user_id_from_gid, get_user_role from ..utils_aiohttp import envelope_json_response from . import nodes_utils, projects_service -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from ._nodes_api import NodeScreenshot, get_node_screenshots from .exceptions import ( diff --git a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py index df44d5865917..7fefd71ce0ca 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py @@ -31,7 +31,7 @@ from ..projects._access_rights_api import check_user_project_permission from ..security.decorators import permission_required from . import _ports_api, projects_service -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from .db import ProjectDBAPI from .models import ProjectDict diff --git a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py index 082fb1135799..ecac998f854f 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py @@ -31,7 +31,7 @@ from ..utils_aiohttp import envelope_json_response from . import api as projects_api from . import projects_service -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext from .exceptions import ProjectStartsTooManyDynamicNodesError diff --git a/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py index d57bbe49756a..49ce6650ca0e 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_tags_handlers.py @@ -13,7 +13,7 @@ from ..login.decorators import login_required from ..security.decorators import permission_required from . import _tags_api as tags_api -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py b/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py index 968da60e5365..49ede54edf3a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py @@ -18,7 +18,7 @@ from ..products.api import get_product_name from ..security.decorators import permission_required from . import _trash_service -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RemoveQueryParams from .exceptions import ProjectRunningConflictError, ProjectStoppingError diff --git a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py index deef8c6d97b4..79a0e6d0cf37 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py @@ -23,7 +23,7 @@ from ..security.decorators import permission_required from . import _wallets_api as wallets_api from . import projects_service -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import ProjectPathParams, RequestContext _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py index aa2ea6ced897..2421670eb6e1 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_workspaces_handlers.py @@ -13,7 +13,7 @@ from ..login.decorators import login_required from ..security.decorators import permission_required from . import _workspaces_api -from ._common.exception_handlers import handle_plugin_requests_exceptions +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import RequestContext _logger = logging.getLogger(__name__) From 8df7f7e659dde76ee72313e390656e44c25b7bd8 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:30:41 +0100 Subject: [PATCH 13/20] errros in api --- .../api/v0/openapi.yaml | 378 ++++++++++++++++++ .../projects/_common/exceptions_handlers.py | 104 +++-- 2 files changed, 446 insertions(+), 36 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index ffb889df5280..812f818b1467 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -3656,6 +3656,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_TaskGet_' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable get: tags: - projects @@ -3743,6 +3785,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Page_ProjectListItem_' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/projects/active: get: tags: @@ -3763,6 +3847,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_ProjectGet_' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/projects/{project_id}: get: tags: @@ -3784,6 +3910,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_ProjectGet_' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable patch: tags: - projects @@ -3806,6 +3974,48 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable delete: tags: - projects @@ -3822,6 +4032,48 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/projects/{project_id}:clone: post: tags: @@ -3843,6 +4095,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_TaskGet_' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/projects:search: get: tags: @@ -3906,6 +4200,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Page_ProjectListItem_' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/projects/{project_id}/inactivity: get: tags: @@ -3927,6 +4263,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_GetProjectInactivityResponse_' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/projects/{project_uuid}/comments: post: tags: diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py index bd3e7bb20d86..f105be67dbc9 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py @@ -36,10 +36,8 @@ _logger = logging.getLogger(__name__) -_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { - # - # NOTE: keep keys alphabetically sorted - # +# Folder errors +_FOLDER_ERRORS: ExceptionToHttpErrorMap = { FolderAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, "Access to folder forbidden", @@ -48,6 +46,10 @@ status.HTTP_404_NOT_FOUND, "Folder not found: {reason}", ), +} + +# Node errors +_NODE_ERRORS: ExceptionToHttpErrorMap = { NodeNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, "Node '{node_uuid}' not found in project '{project_uuid}'", @@ -56,6 +58,14 @@ status.HTTP_404_NOT_FOUND, "Parent node '{node_uuid}' not found", ), + ProjectNodeRequiredInputsNotSetError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "Project node is required but input is not set", + ), +} + +# Project errors +_PROJECT_ERRORS: ExceptionToHttpErrorMap = { ProjectDeleteError: HttpErrorInfo( status.HTTP_409_CONFLICT, "Failed to complete deletion of '{project_uuid}': {reason}", @@ -84,6 +94,30 @@ status.HTTP_409_CONFLICT, "You cannot open more than {max_num_projects} study/ies at once. Please close another study and retry.", ), + ProjectStartsTooManyDynamicNodesError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "The maximal amount of concurrently running dynamic services was reached. Please manually stop a service and retry.", + ), + ProjectWalletPendingTransactionError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "Project has currently pending transactions. It is forbidden to change wallet.", + ), + ProjectInDebtCanNotChangeWalletError: HttpErrorInfo( + status.HTTP_402_PAYMENT_REQUIRED, + "Unable to change the credit account linked to the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + ), + ProjectInDebtCanNotOpenError: HttpErrorInfo( + status.HTTP_402_PAYMENT_REQUIRED, + "Unable to open the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + ), + WrongTagIdsInQueryError: HttpErrorInfo( + status.HTTP_400_BAD_REQUEST, + "Wrong tag IDs in query", + ), +} + +# Workspace errors +_WORKSPACE_ERRORS: ExceptionToHttpErrorMap = { WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, "Access to workspace forbidden: {reason}", @@ -92,14 +126,26 @@ status.HTTP_404_NOT_FOUND, "Workspace not found: {reason}", ), - WrongTagIdsInQueryError: HttpErrorInfo( - status.HTTP_400_BAD_REQUEST, - "Wrong tag IDs in query", - ), +} + +# Wallet errors +_WALLET_ERRORS: ExceptionToHttpErrorMap = { UserDefaultWalletNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, "Wallet not found: {reason}", ), + WalletAccessForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Payment required, but the user lacks access to the project's linked wallet: Wallet access forbidden. {reason}", + ), + WalletNotEnoughCreditsError: HttpErrorInfo( + status.HTTP_402_PAYMENT_REQUIRED, + "Wallet does not have enough credits. {reason}", + ), +} + +# Pricing errors +_PRICING_ERRORS: ExceptionToHttpErrorMap = { DefaultPricingPlanNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, "Default pricing plan not found", @@ -108,43 +154,29 @@ status.HTTP_404_NOT_FOUND, "Default pricing unit not found", ), - WalletNotEnoughCreditsError: HttpErrorInfo( - status.HTTP_402_PAYMENT_REQUIRED, - "Wallet does not have enough credits. {reason}", - ), - ProjectInDebtCanNotChangeWalletError: HttpErrorInfo( - status.HTTP_402_PAYMENT_REQUIRED, - "Unable to change the credit account linked to the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", - ), - ProjectInDebtCanNotOpenError: HttpErrorInfo( - status.HTTP_402_PAYMENT_REQUIRED, - "Unable to open the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", - ), - ProjectStartsTooManyDynamicNodesError: HttpErrorInfo( - status.HTTP_409_CONFLICT, - "The maximal amount of concurrently running dynamic services was reached. Please manually stop a service and retry.", - ), - ProjectWalletPendingTransactionError: HttpErrorInfo( - status.HTTP_409_CONFLICT, - "Project has currently pending transactions. It is forbidden to change wallet.", - ), +} + +# Other errors +_OTHER_ERRORS: ExceptionToHttpErrorMap = { ClustersKeeperNotAvailableError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, "Clusters-keeper service is not available", ), - ProjectNodeRequiredInputsNotSetError: HttpErrorInfo( - status.HTTP_409_CONFLICT, - "Project node is required but input is not set", - ), CatalogForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, "Catalog forbidden: Insufficient access rights for {name}", ), - WalletAccessForbiddenError: HttpErrorInfo( - status.HTTP_403_FORBIDDEN, - "Payment required, but the user lacks access to the project's linked wallet: Wallet access forbidden. {reason}", - ), } + +_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {} +_TO_HTTP_ERROR_MAP.update(_FOLDER_ERRORS) +_TO_HTTP_ERROR_MAP.update(_NODE_ERRORS) +_TO_HTTP_ERROR_MAP.update(_PROJECT_ERRORS) +_TO_HTTP_ERROR_MAP.update(_WORKSPACE_ERRORS) +_TO_HTTP_ERROR_MAP.update(_WALLET_ERRORS) +_TO_HTTP_ERROR_MAP.update(_PRICING_ERRORS) +_TO_HTTP_ERROR_MAP.update(_OTHER_ERRORS) + handle_plugin_requests_exceptions = exception_handling_decorator( to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) ) From b40f42ba0d5555dfb09b8d49aca86e5c1545f92a Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:35:24 +0100 Subject: [PATCH 14/20] comments rest --- .../projects/_comments_handlers.py | 34 ++++++------------- .../projects/_common/exceptions_handlers.py | 14 ++++---- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py index 04ac3d5ca35a..f3c274d13b90 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py @@ -2,7 +2,6 @@ """ -import functools import logging from typing import Any @@ -22,7 +21,6 @@ parse_request_path_parameters_as, parse_request_query_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.rest_constants import RESPONSE_MODEL_POLICY @@ -31,26 +29,11 @@ from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _comments_api, projects_service +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import RequestContext -from .exceptions import ProjectInvalidRightsError, ProjectNotFoundError _logger = logging.getLogger(__name__) - -def _handle_project_comments_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ProjectNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - except ProjectInvalidRightsError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper - - # # projects/*/comments COLLECTION ------------------------- # @@ -79,7 +62,7 @@ class _ProjectCommentsBodyParams(BaseModel): ) @login_required @permission_required("project.read") -@_handle_project_comments_exceptions +@handle_plugin_requests_exceptions async def create_project_comment(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectCommentsPathParams, request) @@ -119,7 +102,7 @@ class _ListProjectCommentsQueryParams(BaseModel): @routes.get(f"/{VTAG}/projects/{{project_uuid}}/comments", name="list_project_comments") @login_required @permission_required("project.read") -@_handle_project_comments_exceptions +@handle_plugin_requests_exceptions async def list_project_comments(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(_ProjectCommentsPathParams, request) @@ -168,6 +151,7 @@ async def list_project_comments(request: web.Request): ) @login_required @permission_required("project.read") +@handle_plugin_requests_exceptions async def update_project_comment(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( @@ -183,12 +167,13 @@ async def update_project_comment(request: web.Request): include_state=False, ) - return await _comments_api.update_project_comment( + updated_comment = await _comments_api.update_project_comment( request=request, comment_id=path_params.comment_id, project_uuid=path_params.project_uuid, contents=body_params.contents, ) + return envelope_json_response(updated_comment) @routes.delete( @@ -197,7 +182,7 @@ async def update_project_comment(request: web.Request): ) @login_required @permission_required("project.read") -@_handle_project_comments_exceptions +@handle_plugin_requests_exceptions async def delete_project_comment(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( @@ -225,7 +210,7 @@ async def delete_project_comment(request: web.Request): ) @login_required @permission_required("project.read") -@_handle_project_comments_exceptions +@handle_plugin_requests_exceptions async def get_project_comment(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as( @@ -240,7 +225,8 @@ async def get_project_comment(request: web.Request): include_state=False, ) - return await _comments_api.get_project_comment( + comment = await _comments_api.get_project_comment( request=request, comment_id=path_params.comment_id, ) + return envelope_json_response(comment) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py index f105be67dbc9..d5dc29b4d762 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py @@ -36,7 +36,7 @@ _logger = logging.getLogger(__name__) -# Folder errors + _FOLDER_ERRORS: ExceptionToHttpErrorMap = { FolderAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, @@ -48,7 +48,7 @@ ), } -# Node errors + _NODE_ERRORS: ExceptionToHttpErrorMap = { NodeNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, @@ -64,7 +64,7 @@ ), } -# Project errors + _PROJECT_ERRORS: ExceptionToHttpErrorMap = { ProjectDeleteError: HttpErrorInfo( status.HTTP_409_CONFLICT, @@ -116,7 +116,7 @@ ), } -# Workspace errors + _WORKSPACE_ERRORS: ExceptionToHttpErrorMap = { WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, @@ -128,7 +128,7 @@ ), } -# Wallet errors + _WALLET_ERRORS: ExceptionToHttpErrorMap = { UserDefaultWalletNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, @@ -144,7 +144,7 @@ ), } -# Pricing errors + _PRICING_ERRORS: ExceptionToHttpErrorMap = { DefaultPricingPlanNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, @@ -156,7 +156,7 @@ ), } -# Other errors + _OTHER_ERRORS: ExceptionToHttpErrorMap = { ClustersKeeperNotAvailableError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, From 68cfd2e051e0aa61766d4fb57a21e3f1263247e9 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:45:19 +0100 Subject: [PATCH 15/20] errors --- .../_projects_nodes_pricing_unit_handlers.py | 23 +++---------------- .../projects/_trash_rest.py | 13 ++--------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py index 66b17383bba7..fe39871840f7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py @@ -2,7 +2,6 @@ """ -import functools import logging from aiohttp import web @@ -13,7 +12,6 @@ from models_library.resource_tracker import PricingPlanId, PricingUnitId from pydantic import BaseModel, ConfigDict from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as -from servicelib.aiohttp.typing_extension import Handler from .._meta import API_VTAG from ..login.decorators import login_required @@ -21,10 +19,10 @@ from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import projects_service +from ._common.exceptions_handlers import handle_plugin_requests_exceptions from ._common.models import RequestContext from ._nodes_handlers import NodePathParams from .db import ProjectDBAPI -from .exceptions import ProjectInvalidRightsError, ProjectNotFoundError _logger = logging.getLogger(__name__) @@ -37,21 +35,6 @@ class PricingUnitNotFoundError(PricingUnitError): msg_template = "Pricing unit not found" -def _handle_projects_nodes_pricing_unit_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except ProjectNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except (PricingUnitNotFoundError, ProjectInvalidRightsError) as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper - - routes = web.RouteTableDef() @@ -61,7 +44,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: ) @login_required @permission_required("project.wallet.*") -@_handle_projects_nodes_pricing_unit_exceptions +@handle_plugin_requests_exceptions async def get_project_node_pricing_unit(request: web.Request): db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) req_ctx = RequestContext.model_validate(request) @@ -108,7 +91,7 @@ class _ProjectNodePricingUnitPathParams(BaseModel): ) @login_required @permission_required("project.wallet.*") -@_handle_projects_nodes_pricing_unit_exceptions +@handle_plugin_requests_exceptions async def connect_pricing_unit_to_project_node(request: web.Request): db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) req_ctx = RequestContext.model_validate(request) diff --git a/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py b/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py index 49ede54edf3a..daa6010d0f44 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_trash_rest.py @@ -24,12 +24,8 @@ _logger = logging.getLogger(__name__) -# -# LOCAL EXCEPTIONS HANDLING -# - -_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { +_TRASH_ERRORS: ExceptionToHttpErrorMap = { ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, "Current study is in use and cannot be trashed [project_id={project_uuid}]. Please stop all services first and try again", @@ -40,16 +36,11 @@ ), } - _handle_local_request_exceptions = exception_handling_decorator( - to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP) + to_exceptions_handlers_map(_TRASH_ERRORS) ) -# -# ROUTES -# - routes = web.RouteTableDef() From 086a8a2e9587e9827d0d59d8f72fe62ac42b2ebf Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:45:30 +0100 Subject: [PATCH 16/20] errors in API --- api/specs/web-server/_trash.py | 20 +- .../api/v0/openapi.yaml | 252 ++++++++++++++++++ 2 files changed, 269 insertions(+), 3 deletions(-) diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index 203c05a4c9e7..193d994d55cd 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -8,12 +8,16 @@ from typing import Annotated from fastapi import APIRouter, Depends, status +from models_library.rest_error import EnvelopedError from models_library.trash import RemoveQueryParams from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.folders._common.models import ( FoldersPathParams, FolderTrashQueryParams, ) +from simcore_service_webserver.projects._common.exceptions_handlers import ( + _TO_HTTP_ERROR_MAP, +) from simcore_service_webserver.projects._trash_rest import ProjectPathParams from simcore_service_webserver.workspaces._common.models import ( WorkspacesPathParams, @@ -23,6 +27,9 @@ router = APIRouter( prefix=f"/{API_VTAG}", tags=["trash"], + responses={ + i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values() + }, ) @@ -42,11 +49,18 @@ def empty_trash(): tags=_extra_tags, status_code=status.HTTP_204_NO_CONTENT, responses={ - status.HTTP_404_NOT_FOUND: {"description": "Not such a project"}, + status.HTTP_404_NOT_FOUND: { + "description": "Not such a project", + "model": EnvelopedError, + }, status.HTTP_409_CONFLICT: { - "description": "Project is in use and cannot be trashed" + "description": "Project is in use and cannot be trashed", + "model": EnvelopedError, + }, + status.HTTP_503_SERVICE_UNAVAILABLE: { + "description": "Trash service error", + "model": EnvelopedError, }, - status.HTTP_503_SERVICE_UNAVAILABLE: {"description": "Trash service error"}, }, ) def trash_project( diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 812f818b1467..584614e6050b 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -6360,6 +6360,48 @@ paths: responses: '204': description: Successful Response + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + '409': + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + '422': + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + '402': + description: Payment Required + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + '503': + description: Service Unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' /v0/projects/{project_id}:trash: post: tags: @@ -6385,12 +6427,48 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden '404': description: Not such a project + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' '409': description: Project is in use and cannot be trashed + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required '503': description: Trash service error + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' /v0/projects/{project_id}:untrash: post: tags: @@ -6409,6 +6487,48 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/folders/{folder_id}:trash: post: tags: @@ -6435,11 +6555,35 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden '404': description: Not such a folder '409': description: One or more projects in the folder are in use and cannot be trashed + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required '503': description: Trash service error /v0/folders/{folder_id}:untrash: @@ -6461,6 +6605,48 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/workspaces/{workspace_id}:trash: post: tags: @@ -6487,11 +6673,35 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden '404': description: Not such a workspace '409': description: One or more projects in the workspace are in use and cannot be trashed + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required '503': description: Trash service error /v0/workspaces/{workspace_id}:untrash: @@ -6513,6 +6723,48 @@ paths: responses: '204': description: Successful Response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Forbidden + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Unprocessable Entity + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Bad Request + '402': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Payment Required + '503': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvelopedError' + description: Service Unavailable /v0/workspaces: post: tags: From 0be980e823947ca3d36ddd16d0454bd518b8434c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:13:27 +0100 Subject: [PATCH 17/20] fixes tests --- .../projects/_common/exceptions_handlers.py | 8 +++++++- .../with_dbs/02/test_projects_nodes_handlers__patch.py | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py index d5dc29b4d762..bb7128a5f458 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py @@ -1,7 +1,10 @@ import logging from servicelib.aiohttp import status -from servicelib.rabbitmq.rpc_interfaces.catalog.errors import CatalogForbiddenError +from servicelib.rabbitmq.rpc_interfaces.catalog.errors import ( + CatalogForbiddenError, + CatalogItemNotFoundError, +) from ...exception_handling import ( ExceptionToHttpErrorMap, @@ -166,6 +169,9 @@ status.HTTP_403_FORBIDDEN, "Catalog forbidden: Insufficient access rights for {name}", ), + CatalogItemNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, "{name} was not found" + ), } _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {} 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 1f6f18cc00a3..77890e530c8d 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 @@ -356,7 +356,7 @@ async def test_patch_project_node_service_key_with_error( ): node_id = next(iter(user_project["workbench"])) assert client.app - base_url = client.app.router["patch_project_node"].url_for( + url = client.app.router["patch_project_node"].url_for( project_id=user_project["uuid"], node_id=node_id ) _patch_version = {"version": "2.0.9"} @@ -365,12 +365,12 @@ async def test_patch_project_node_service_key_with_error( "simcore_service_webserver.projects.projects_service.catalog_rpc.check_for_service", side_effect=CatalogForbiddenError(name="test"), ): - resp = await client.patch(f"{base_url}", json=_patch_version) + resp = await client.patch(f"{url}", json=_patch_version) assert resp.status == status.HTTP_403_FORBIDDEN with mocker.patch( "simcore_service_webserver.projects.projects_service.catalog_rpc.check_for_service", side_effect=CatalogItemNotFoundError(name="test"), ): - resp = await client.patch(f"{base_url}", json=_patch_version) + resp = await client.patch(f"{url}", json=_patch_version) assert resp.status == status.HTTP_404_NOT_FOUND From 08762379ab8baf31c30b0e6b148e88674f04418e Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:47:00 +0100 Subject: [PATCH 18/20] fixes tash --- .../projects/_trash_service.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_trash_service.py b/services/web/server/src/simcore_service_webserver/projects/_trash_service.py index 77358dbd7764..7f3bf1f9adaa 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_trash_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_trash_service.py @@ -192,11 +192,13 @@ async def list_trashed_projects( # This filtering couldn't be handled at the database level when `projects_repo` # was refactored, as defining a custom trash_filter was needed to allow more # flexibility in filtering options. - trashed_projects = [ - project["uuid"] - for project in projects - if _can_delete(project, user_id, until_equal_datetime) - ] + trashed_projects.extend( + [ + project["uuid"] + for project in projects + if _can_delete(project, user_id, until_equal_datetime) + ] + ) return trashed_projects From 9f32899f03e561fa485f736f45910e6e79bede98 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:53:05 +0100 Subject: [PATCH 19/20] fixes tests --- .../tests/unit/with_dbs/02/test_projects_metadata_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py index ce87970f75c6..dae450a88fe7 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_metadata_handlers.py @@ -63,7 +63,7 @@ async def test_custom_metadata_handlers( response = await client.get(f"{url}") _, error = await assert_status(response, expected_status_code=expected.not_found) - error_message = error["errors"][0]["message"] + error_message = error["message"] assert invalid_project_id in error_message assert "project" in error_message.lower() From 62523362796772c13516cff6c65fe37012993b9d Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:09:23 +0100 Subject: [PATCH 20/20] @GitHK review: dict --- .../projects/_common/exceptions_handlers.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py index bb7128a5f458..14963f6ca534 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_common/exceptions_handlers.py @@ -174,14 +174,15 @@ ), } -_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {} -_TO_HTTP_ERROR_MAP.update(_FOLDER_ERRORS) -_TO_HTTP_ERROR_MAP.update(_NODE_ERRORS) -_TO_HTTP_ERROR_MAP.update(_PROJECT_ERRORS) -_TO_HTTP_ERROR_MAP.update(_WORKSPACE_ERRORS) -_TO_HTTP_ERROR_MAP.update(_WALLET_ERRORS) -_TO_HTTP_ERROR_MAP.update(_PRICING_ERRORS) -_TO_HTTP_ERROR_MAP.update(_OTHER_ERRORS) +_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { + **_FOLDER_ERRORS, + **_NODE_ERRORS, + **_OTHER_ERRORS, + **_PRICING_ERRORS, + **_PROJECT_ERRORS, + **_WALLET_ERRORS, + **_WORKSPACE_ERRORS, +} handle_plugin_requests_exceptions = exception_handling_decorator( to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP)