Skip to content

Commit 7513aea

Browse files
committed
error handling in ports_rest
1 parent 159a39e commit 7513aea

File tree

2 files changed

+29
-122
lines changed

2 files changed

+29
-122
lines changed

services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py

Lines changed: 15 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
import asyncio
6-
import functools
76
import logging
87

98
from aiohttp import web
@@ -42,17 +41,12 @@
4241
parse_request_path_parameters_as,
4342
parse_request_query_parameters_as,
4443
)
45-
from servicelib.aiohttp.typing_extension import Handler
4644
from servicelib.common_headers import (
4745
UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE,
4846
X_SIMCORE_USER_AGENT,
4947
)
5048
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
5149
from servicelib.rabbitmq import RPCServerError
52-
from servicelib.rabbitmq.rpc_interfaces.catalog.errors import (
53-
CatalogForbiddenError,
54-
CatalogItemNotFoundError,
55-
)
5650
from servicelib.rabbitmq.rpc_interfaces.dynamic_scheduler.errors import (
5751
ServiceWaitingForManualInterventionError,
5852
ServiceWasNotFoundError,
@@ -67,70 +61,22 @@
6761
from ..groups.exceptions import GroupNotFoundError
6862
from ..login.decorators import login_required
6963
from ..projects.api import has_user_project_access_rights
70-
from ..resource_usage.errors import DefaultPricingPlanNotFoundError
7164
from ..security.decorators import permission_required
7265
from ..users.api import get_user_id_from_gid, get_user_role
73-
from ..users.exceptions import UserDefaultWalletNotFoundError
7466
from ..utils_aiohttp import envelope_json_response
75-
from ..wallets.errors import WalletAccessForbiddenError, WalletNotEnoughCreditsError
7667
from . import nodes_utils, projects_service
68+
from ._common.exception_handlers import handle_plugin_requests_exceptions
7769
from ._common.models import ProjectPathParams, RequestContext
7870
from ._nodes_api import NodeScreenshot, get_node_screenshots
7971
from .exceptions import (
80-
ClustersKeeperNotAvailableError,
81-
DefaultPricingUnitNotFoundError,
8272
NodeNotFoundError,
83-
ProjectInDebtCanNotChangeWalletError,
84-
ProjectInvalidRightsError,
85-
ProjectNodeRequiredInputsNotSetError,
8673
ProjectNodeResourcesInsufficientRightsError,
8774
ProjectNodeResourcesInvalidError,
88-
ProjectNotFoundError,
89-
ProjectStartsTooManyDynamicNodesError,
9075
)
9176

9277
_logger = logging.getLogger(__name__)
9378

9479

95-
def _handle_project_nodes_exceptions(handler: Handler):
96-
@functools.wraps(handler)
97-
async def wrapper(request: web.Request) -> web.StreamResponse:
98-
try:
99-
return await handler(request)
100-
101-
except (
102-
ProjectNotFoundError,
103-
NodeNotFoundError,
104-
UserDefaultWalletNotFoundError,
105-
DefaultPricingPlanNotFoundError,
106-
DefaultPricingUnitNotFoundError,
107-
GroupNotFoundError,
108-
CatalogItemNotFoundError,
109-
) as exc:
110-
raise web.HTTPNotFound(reason=f"{exc}") from exc
111-
except (
112-
WalletNotEnoughCreditsError,
113-
ProjectInDebtCanNotChangeWalletError,
114-
) as exc:
115-
raise web.HTTPPaymentRequired(reason=f"{exc}") from exc
116-
except ProjectInvalidRightsError as exc:
117-
raise web.HTTPUnauthorized(reason=f"{exc}") from exc
118-
except ProjectStartsTooManyDynamicNodesError as exc:
119-
raise web.HTTPConflict(reason=f"{exc}") from exc
120-
except ClustersKeeperNotAvailableError as exc:
121-
raise web.HTTPServiceUnavailable(reason=f"{exc}") from exc
122-
except ProjectNodeRequiredInputsNotSetError as exc:
123-
raise web.HTTPConflict(reason=f"{exc}") from exc
124-
except CatalogForbiddenError as exc:
125-
raise web.HTTPForbidden(reason=f"{exc}") from exc
126-
except WalletAccessForbiddenError as exc:
127-
raise web.HTTPForbidden(
128-
reason=f"Payment required, but the user lacks access to the project's linked wallet.: {exc}"
129-
) from exc
130-
131-
return wrapper
132-
133-
13480
#
13581
# projects/*/nodes COLLECTION -------------------------
13682
#
@@ -145,7 +91,7 @@ class NodePathParams(ProjectPathParams):
14591
@routes.post(f"/{VTAG}/projects/{{project_id}}/nodes", name="create_node")
14692
@login_required
14793
@permission_required("project.node.create")
148-
@_handle_project_nodes_exceptions
94+
@handle_plugin_requests_exceptions
14995
async def create_node(request: web.Request) -> web.Response:
15096
req_ctx = RequestContext.model_validate(request)
15197
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
@@ -187,7 +133,7 @@ async def create_node(request: web.Request) -> web.Response:
187133
@routes.get(f"/{VTAG}/projects/{{project_id}}/nodes/{{node_id}}", name="get_node")
188134
@login_required
189135
@permission_required("project.node.read")
190-
@_handle_project_nodes_exceptions
136+
@handle_plugin_requests_exceptions
191137
# NOTE: Careful, this endpoint is actually "get_node_state," and it doesn't return a Node resource.
192138
async def get_node(request: web.Request) -> web.Response:
193139
req_ctx = RequestContext.model_validate(request)
@@ -226,7 +172,7 @@ async def get_node(request: web.Request) -> web.Response:
226172
)
227173
@login_required
228174
@permission_required("project.node.update")
229-
@_handle_project_nodes_exceptions
175+
@handle_plugin_requests_exceptions
230176
async def patch_project_node(request: web.Request) -> web.Response:
231177
req_ctx = RequestContext.model_validate(request)
232178
path_params = parse_request_path_parameters_as(NodePathParams, request)
@@ -247,7 +193,7 @@ async def patch_project_node(request: web.Request) -> web.Response:
247193
@routes.delete(f"/{VTAG}/projects/{{project_id}}/nodes/{{node_id}}", name="delete_node")
248194
@login_required
249195
@permission_required("project.node.delete")
250-
@_handle_project_nodes_exceptions
196+
@handle_plugin_requests_exceptions
251197
async def delete_node(request: web.Request) -> web.Response:
252198
req_ctx = RequestContext.model_validate(request)
253199
path_params = parse_request_path_parameters_as(NodePathParams, request)
@@ -275,7 +221,7 @@ async def delete_node(request: web.Request) -> web.Response:
275221
)
276222
@login_required
277223
@permission_required("project.node.read")
278-
@_handle_project_nodes_exceptions
224+
@handle_plugin_requests_exceptions
279225
async def retrieve_node(request: web.Request) -> web.Response:
280226
"""Has only effect on nodes associated to dynamic services"""
281227
path_params = parse_request_path_parameters_as(NodePathParams, request)
@@ -295,7 +241,7 @@ async def retrieve_node(request: web.Request) -> web.Response:
295241
)
296242
@login_required
297243
@permission_required("project.node.update")
298-
@_handle_project_nodes_exceptions
244+
@handle_plugin_requests_exceptions
299245
async def update_node_outputs(request: web.Request) -> web.Response:
300246
req_ctx = RequestContext.model_validate(request)
301247
path_params = parse_request_path_parameters_as(NodePathParams, request)
@@ -322,7 +268,7 @@ async def update_node_outputs(request: web.Request) -> web.Response:
322268
)
323269
@login_required
324270
@permission_required("project.update")
325-
@_handle_project_nodes_exceptions
271+
@handle_plugin_requests_exceptions
326272
async def start_node(request: web.Request) -> web.Response:
327273
"""Has only effect on nodes associated to dynamic services"""
328274
req_ctx = RequestContext.model_validate(request)
@@ -366,7 +312,7 @@ async def _stop_dynamic_service_task(
366312
)
367313
@login_required
368314
@permission_required("project.update")
369-
@_handle_project_nodes_exceptions
315+
@handle_plugin_requests_exceptions
370316
async def stop_node(request: web.Request) -> web.Response:
371317
"""Has only effect on nodes associated to dynamic services"""
372318
req_ctx = RequestContext.model_validate(request)
@@ -408,7 +354,7 @@ async def stop_node(request: web.Request) -> web.Response:
408354
)
409355
@login_required
410356
@permission_required("project.node.read")
411-
@_handle_project_nodes_exceptions
357+
@handle_plugin_requests_exceptions
412358
async def restart_node(request: web.Request) -> web.Response:
413359
"""Has only effect on nodes associated to dynamic services"""
414360

@@ -432,7 +378,7 @@ async def restart_node(request: web.Request) -> web.Response:
432378
)
433379
@login_required
434380
@permission_required("project.node.read")
435-
@_handle_project_nodes_exceptions
381+
@handle_plugin_requests_exceptions
436382
async def get_node_resources(request: web.Request) -> web.Response:
437383
req_ctx = RequestContext.model_validate(request)
438384
path_params = parse_request_path_parameters_as(NodePathParams, request)
@@ -465,7 +411,7 @@ async def get_node_resources(request: web.Request) -> web.Response:
465411
)
466412
@login_required
467413
@permission_required("project.node.update")
468-
@_handle_project_nodes_exceptions
414+
@handle_plugin_requests_exceptions
469415
async def replace_node_resources(request: web.Request) -> web.Response:
470416
req_ctx = RequestContext.model_validate(request)
471417
path_params = parse_request_path_parameters_as(NodePathParams, request)
@@ -524,7 +470,7 @@ class _ProjectGroupAccess(BaseModel):
524470
)
525471
@login_required
526472
@permission_required("project.read")
527-
@_handle_project_nodes_exceptions
473+
@handle_plugin_requests_exceptions
528474
async def get_project_services_access_for_gid(
529475
request: web.Request,
530476
) -> web.Response:
@@ -644,7 +590,7 @@ class _ProjectNodePreview(BaseModel):
644590
)
645591
@login_required
646592
@permission_required("project.read")
647-
@_handle_project_nodes_exceptions
593+
@handle_plugin_requests_exceptions
648594
async def list_project_nodes_previews(request: web.Request) -> web.Response:
649595
req_ctx = RequestContext.model_validate(request)
650596
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
@@ -684,7 +630,7 @@ async def list_project_nodes_previews(request: web.Request) -> web.Response:
684630
)
685631
@login_required
686632
@permission_required("project.read")
687-
@_handle_project_nodes_exceptions
633+
@handle_plugin_requests_exceptions
688634
async def get_project_node_preview(request: web.Request) -> web.Response:
689635
req_ctx = RequestContext.model_validate(request)
690636
path_params = parse_request_path_parameters_as(NodePathParams, request)

services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
- /projects/{*}/outputs
44
"""
55

6-
import functools
76
import logging
8-
from collections.abc import Awaitable, Callable
97
from typing import Any, Literal
108

119
from aiohttp import web
12-
from common_library.json_serialization import json_dumps
1310
from models_library.api_schemas_webserver.projects_ports import (
1411
ProjectInputGet,
1512
ProjectInputUpdate,
@@ -27,57 +24,21 @@
2724
parse_request_body_as,
2825
parse_request_path_parameters_as,
2926
)
27+
from simcore_service_webserver.utils_aiohttp import envelope_json_response
3028

3129
from .._meta import API_VTAG as VTAG
3230
from ..login.decorators import login_required
3331
from ..projects._access_rights_api import check_user_project_permission
3432
from ..security.decorators import permission_required
3533
from . import _ports_api, projects_service
34+
from ._common.exception_handlers import handle_plugin_requests_exceptions
3635
from ._common.models import ProjectPathParams, RequestContext
3736
from .db import ProjectDBAPI
38-
from .exceptions import (
39-
NodeNotFoundError,
40-
ProjectInvalidRightsError,
41-
ProjectNotFoundError,
42-
)
4337
from .models import ProjectDict
4438

4539
log = logging.getLogger(__name__)
4640

4741

48-
def _web_json_response_enveloped(data: Any) -> web.Response:
49-
return web.json_response(
50-
{
51-
"data": jsonable_encoder(data),
52-
},
53-
dumps=json_dumps,
54-
)
55-
56-
57-
def _handle_project_exceptions(
58-
handler: Callable[[web.Request], Awaitable[web.Response]]
59-
) -> Callable[[web.Request], Awaitable[web.Response]]:
60-
@functools.wraps(handler)
61-
async def wrapper(request: web.Request) -> web.Response:
62-
try:
63-
return await handler(request)
64-
65-
except ProjectNotFoundError as exc:
66-
raise web.HTTPNotFound(
67-
reason=f"Project '{exc.project_uuid}' not found"
68-
) from exc
69-
70-
except ProjectInvalidRightsError as exc:
71-
raise web.HTTPUnauthorized from exc
72-
73-
except NodeNotFoundError as exc:
74-
raise web.HTTPNotFound(
75-
reason=f"Port '{exc.node_uuid}' not found in node '{exc.project_uuid}'"
76-
) from exc
77-
78-
return wrapper
79-
80-
8142
async def _get_validated_workbench_model(
8243
app: web.Application, project_id: ProjectID, user_id: UserID
8344
) -> dict[NodeID, Node]:
@@ -101,7 +62,7 @@ async def _get_validated_workbench_model(
10162
@routes.get(f"/{VTAG}/projects/{{project_id}}/inputs", name="get_project_inputs")
10263
@login_required
10364
@permission_required("project.read")
104-
@_handle_project_exceptions
65+
@handle_plugin_requests_exceptions
10566
async def get_project_inputs(request: web.Request) -> web.Response:
10667
req_ctx = RequestContext.model_validate(request)
10768
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
@@ -113,8 +74,8 @@ async def get_project_inputs(request: web.Request) -> web.Response:
11374
)
11475
inputs: dict[NodeID, Any] = _ports_api.get_project_inputs(workbench)
11576

116-
return _web_json_response_enveloped(
117-
data={
77+
return envelope_json_response(
78+
{
11879
node_id: ProjectInputGet(
11980
key=node_id, label=workbench[node_id].label, value=value
12081
)
@@ -126,7 +87,7 @@ async def get_project_inputs(request: web.Request) -> web.Response:
12687
@routes.patch(f"/{VTAG}/projects/{{project_id}}/inputs", name="update_project_inputs")
12788
@login_required
12889
@permission_required("project.update")
129-
@_handle_project_exceptions
90+
@handle_plugin_requests_exceptions
13091
async def update_project_inputs(request: web.Request) -> web.Response:
13192
db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app)
13293
req_ctx = RequestContext.model_validate(request)
@@ -174,8 +135,8 @@ async def update_project_inputs(request: web.Request) -> web.Response:
174135
)
175136
inputs: dict[NodeID, Any] = _ports_api.get_project_inputs(workbench)
176137

177-
return _web_json_response_enveloped(
178-
data={
138+
return envelope_json_response(
139+
{
179140
node_id: ProjectInputGet(
180141
key=node_id, label=workbench[node_id].label, value=value
181142
)
@@ -192,7 +153,7 @@ async def update_project_inputs(request: web.Request) -> web.Response:
192153
@routes.get(f"/{VTAG}/projects/{{project_id}}/outputs", name="get_project_outputs")
193154
@login_required
194155
@permission_required("project.read")
195-
@_handle_project_exceptions
156+
@handle_plugin_requests_exceptions
196157
async def get_project_outputs(request: web.Request) -> web.Response:
197158
req_ctx = RequestContext.model_validate(request)
198159
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
@@ -206,8 +167,8 @@ async def get_project_outputs(request: web.Request) -> web.Response:
206167
request.app, project_id=path_params.project_id, workbench=workbench
207168
)
208169

209-
return _web_json_response_enveloped(
210-
data={
170+
return envelope_json_response(
171+
{
211172
node_id: ProjectOutputGet(
212173
key=node_id, label=workbench[node_id].label, value=value
213174
)
@@ -239,7 +200,7 @@ class ProjectMetadataPortGet(BaseModel):
239200
)
240201
@login_required
241202
@permission_required("project.read")
242-
@_handle_project_exceptions
203+
@handle_plugin_requests_exceptions
243204
async def list_project_metadata_ports(request: web.Request) -> web.Response:
244205
req_ctx = RequestContext.model_validate(request)
245206
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
@@ -250,8 +211,8 @@ async def list_project_metadata_ports(request: web.Request) -> web.Response:
250211
app=request.app, project_id=path_params.project_id, user_id=req_ctx.user_id
251212
)
252213

253-
return _web_json_response_enveloped(
254-
data=[
214+
return envelope_json_response(
215+
[
255216
ProjectMetadataPortGet(
256217
key=port.node_id,
257218
kind=port.kind,

0 commit comments

Comments
 (0)