44
55"""
66
7- import functools
87import logging
98
109from aiohttp import web
3130 parse_request_path_parameters_as ,
3231 parse_request_query_parameters_as ,
3332)
34- from servicelib .aiohttp .typing_extension import Handler
3533from servicelib .common_headers import (
3634 UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE ,
3735 X_SIMCORE_USER_AGENT ,
4341
4442from .._meta import API_VTAG as VTAG
4543from ..catalog .client import get_services_for_user_in_product
46- from ..folders .errors import FolderAccessForbiddenError , FolderNotFoundError
4744from ..login .decorators import login_required
4845from ..redis import get_redis_lock_manager_client_sdk
4946from ..resource_manager .user_sessions import PROJECT_ID_KEY , managed_resource
5047from ..security .api import check_user_permission
5148from ..security .decorators import permission_required
5249from ..users .api import get_user_fullname
53- from ..workspaces .errors import WorkspaceAccessForbiddenError , WorkspaceNotFoundError
5450from . import _crud_api_create , _crud_api_read , projects_service
51+ from ._common .exception_handlers import handle_plugin_requests_exceptions
5552from ._common .models import ProjectPathParams , RequestContext
5653from ._crud_handlers_models import (
5754 ProjectActiveQueryParams ,
6259 ProjectsSearchQueryParams ,
6360)
6461from ._permalink_api import update_or_pop_permalink_in_project
65- from .exceptions import (
66- ProjectDeleteError ,
67- ProjectInvalidRightsError ,
68- ProjectNotFoundError ,
69- ProjectOwnerNotFoundInTheProjectAccessRightsError ,
70- WrongTagIdsInQueryError ,
71- )
7262from .utils import get_project_unavailable_services , project_uses_available_services
7363
7464# When the user requests a project with a repo, the working copy might differ from
7767# response needs to refer to the uuid of the request and this is passed through this request key
7868RQ_REQUESTED_REPO_PROJECT_UUID_KEY = f"{ __name__ } .RQT_REQUESTED_REPO_PROJECT_UUID_KEY"
7969
80-
8170_logger = logging .getLogger (__name__ )
8271
8372
84- def _handle_projects_exceptions (handler : Handler ):
85- @functools .wraps (handler )
86- async def _wrapper (request : web .Request ) -> web .StreamResponse :
87- try :
88- return await handler (request )
89-
90- except (
91- ProjectNotFoundError ,
92- FolderNotFoundError ,
93- WorkspaceNotFoundError ,
94- ) as exc :
95- raise web .HTTPNotFound (reason = f"{ exc } " ) from exc
96- except (
97- ProjectOwnerNotFoundInTheProjectAccessRightsError ,
98- WrongTagIdsInQueryError ,
99- ) as exc :
100- raise web .HTTPBadRequest (reason = f"{ exc } " ) from exc
101- except (
102- ProjectInvalidRightsError ,
103- FolderAccessForbiddenError ,
104- WorkspaceAccessForbiddenError ,
105- ) as exc :
106- raise web .HTTPForbidden (reason = f"{ exc } " ) from exc
107-
108- return _wrapper
109-
110-
11173routes = web .RouteTableDef ()
11274
11375
11476@routes .post (f"/{ VTAG } /projects" , name = "create_project" )
11577@login_required
11678@permission_required ("project.create" )
11779@permission_required ("services.pipeline.*" ) # due to update_pipeline_db
118- @_handle_projects_exceptions
80+ @handle_plugin_requests_exceptions
11981async def create_project (request : web .Request ):
12082 #
12183 # - Create https://google.aip.dev/133
@@ -191,7 +153,7 @@ def _create_page_response(projects, request_url, total, limit, offset) -> web.Re
191153@routes .get (f"/{ VTAG } /projects" , name = "list_projects" )
192154@login_required
193155@permission_required ("project.read" )
194- @_handle_projects_exceptions
156+ @handle_plugin_requests_exceptions
195157async def list_projects (request : web .Request ):
196158 #
197159 # - List https://google.aip.dev/132
@@ -244,7 +206,7 @@ async def list_projects(request: web.Request):
244206@routes .get (f"/{ VTAG } /projects:search" , name = "list_projects_full_search" )
245207@login_required
246208@permission_required ("project.read" )
247- @_handle_projects_exceptions
209+ @handle_plugin_requests_exceptions
248210async def list_projects_full_search (request : web .Request ):
249211 req_ctx = RequestContext .model_validate (request )
250212 query_params : ProjectsSearchQueryParams = parse_request_query_parameters_as (
@@ -284,7 +246,7 @@ async def list_projects_full_search(request: web.Request):
284246@routes .get (f"/{ VTAG } /projects/active" , name = "get_active_project" )
285247@login_required
286248@permission_required ("project.read" )
287- @_handle_projects_exceptions
249+ @handle_plugin_requests_exceptions
288250async def get_active_project (request : web .Request ) -> web .Response :
289251 #
290252 # - Get https://google.aip.dev/131
@@ -301,39 +263,35 @@ async def get_active_project(request: web.Request) -> web.Response:
301263 ProjectActiveQueryParams , request
302264 )
303265
304- try :
305- user_active_projects = []
306- with managed_resource (
307- req_ctx .user_id , query_params .client_session_id , request .app
308- ) as rt :
309- # get user's projects
310- user_active_projects = await rt .find (PROJECT_ID_KEY )
311-
312- data = None
313- if user_active_projects :
314- project = await projects_service .get_project_for_user (
315- request .app ,
316- project_uuid = user_active_projects [0 ],
317- user_id = req_ctx .user_id ,
318- include_state = True ,
319- include_trashed_by_primary_gid = True ,
320- )
266+ user_active_projects = []
267+ with managed_resource (
268+ req_ctx .user_id , query_params .client_session_id , request .app
269+ ) as rt :
270+ # get user's projects
271+ user_active_projects = await rt .find (PROJECT_ID_KEY )
321272
322- # updates project's permalink field
323- await update_or_pop_permalink_in_project (request , project )
273+ data = None
274+ if user_active_projects :
275+ project = await projects_service .get_project_for_user (
276+ request .app ,
277+ project_uuid = user_active_projects [0 ],
278+ user_id = req_ctx .user_id ,
279+ include_state = True ,
280+ include_trashed_by_primary_gid = True ,
281+ )
324282
325- data = ProjectGet .from_domain_model (project ).data (exclude_unset = True )
283+ # updates project's permalink field
284+ await update_or_pop_permalink_in_project (request , project )
326285
327- return web . json_response ({ " data" : data }, dumps = json_dumps )
286+ data = ProjectGet . from_domain_model ( project ). data ( exclude_unset = True )
328287
329- except ProjectNotFoundError as exc :
330- raise web .HTTPNotFound (reason = "Project not found" ) from exc
288+ return web .json_response ({"data" : data }, dumps = json_dumps )
331289
332290
333291@routes .get (f"/{ VTAG } /projects/{{project_id}}" , name = "get_project" )
334292@login_required
335293@permission_required ("project.read" )
336- @_handle_projects_exceptions
294+ @handle_plugin_requests_exceptions
337295async def get_project (request : web .Request ):
338296 """
339297
@@ -351,54 +309,44 @@ async def get_project(request: web.Request):
351309 request .app , req_ctx .user_id , req_ctx .product_name , only_key_versions = True
352310 )
353311
354- try :
355- project = await projects_service .get_project_for_user (
356- request .app ,
357- project_uuid = f"{ path_params .project_id } " ,
358- user_id = req_ctx .user_id ,
359- include_state = True ,
360- include_trashed_by_primary_gid = True ,
312+ project = await projects_service .get_project_for_user (
313+ request .app ,
314+ project_uuid = f"{ path_params .project_id } " ,
315+ user_id = req_ctx .user_id ,
316+ include_state = True ,
317+ include_trashed_by_primary_gid = True ,
318+ )
319+ if not await project_uses_available_services (project , user_available_services ):
320+ unavilable_services = get_project_unavailable_services (
321+ project , user_available_services
361322 )
362- if not await project_uses_available_services (project , user_available_services ):
363- unavilable_services = get_project_unavailable_services (
364- project , user_available_services
365- )
366- formatted_services = ", " .join (
367- f"{ service } :{ version } " for service , version in unavilable_services
368- )
369- # TODO: lack of permissions should be notified with https://httpstatuses.com/403 web.HTTPForbidden
370- raise web .HTTPNotFound (
371- reason = (
372- f"Project '{ path_params .project_id } ' uses unavailable services. Please ask "
373- f"for permission for the following services { formatted_services } "
374- )
323+ formatted_services = ", " .join (
324+ f"{ service } :{ version } " for service , version in unavilable_services
325+ )
326+ # TODO: lack of permissions should be notified with https://httpstatuses.com/403 web.HTTPForbidden
327+ raise web .HTTPNotFound (
328+ reason = (
329+ f"Project '{ path_params .project_id } ' uses unavailable services. Please ask "
330+ f"for permission for the following services { formatted_services } "
375331 )
332+ )
376333
377- if new_uuid := request .get (RQ_REQUESTED_REPO_PROJECT_UUID_KEY ):
378- project ["uuid" ] = new_uuid
379-
380- # Adds permalink
381- await update_or_pop_permalink_in_project (request , project )
334+ if new_uuid := request .get (RQ_REQUESTED_REPO_PROJECT_UUID_KEY ):
335+ project ["uuid" ] = new_uuid
382336
383- data = ProjectGet . from_domain_model ( project ). data ( exclude_unset = True )
384- return web . json_response ({ "data" : data }, dumps = json_dumps )
337+ # Adds permalink
338+ await update_or_pop_permalink_in_project ( request , project )
385339
386- except ProjectInvalidRightsError as exc :
387- raise web .HTTPForbidden (
388- reason = f"You do not have sufficient rights to read project { path_params .project_id } "
389- ) from exc
390- except ProjectNotFoundError as exc :
391- raise web .HTTPNotFound (
392- reason = f"Project { path_params .project_id } not found"
393- ) from exc
340+ data = ProjectGet .from_domain_model (project ).data (exclude_unset = True )
341+ return web .json_response ({"data" : data }, dumps = json_dumps )
394342
395343
396344@routes .get (
397345 f"/{ VTAG } /projects/{{project_id}}/inactivity" , name = "get_project_inactivity"
398346)
399347@login_required
400348@permission_required ("project.read" )
401- @_handle_projects_exceptions
349+ @handle_plugin_requests_exceptions
402350async def get_project_inactivity (request : web .Request ):
403351 path_params = parse_request_path_parameters_as (ProjectPathParams , request )
404352
@@ -412,7 +360,7 @@ async def get_project_inactivity(request: web.Request):
412360@login_required
413361@permission_required ("project.update" )
414362@permission_required ("services.pipeline.*" )
415- @_handle_projects_exceptions
363+ @handle_plugin_requests_exceptions
416364async def patch_project (request : web .Request ):
417365 #
418366 # Update https://google.aip.dev/134
@@ -435,7 +383,7 @@ async def patch_project(request: web.Request):
435383@routes .delete (f"/{ VTAG } /projects/{{project_id}}" , name = "delete_project" )
436384@login_required
437385@permission_required ("project.delete" )
438- @_handle_projects_exceptions
386+ @handle_plugin_requests_exceptions
439387async def delete_project (request : web .Request ):
440388 # Delete https://google.aip.dev/135
441389 """
@@ -453,64 +401,52 @@ async def delete_project(request: web.Request):
453401 req_ctx = RequestContext .model_validate (request )
454402 path_params = parse_request_path_parameters_as (ProjectPathParams , request )
455403
456- try :
457- await projects_service .get_project_for_user (
458- request .app ,
459- project_uuid = f"{ path_params .project_id } " ,
460- user_id = req_ctx .user_id ,
461- )
462- project_users : set [int ] = set ()
463- with managed_resource (req_ctx .user_id , None , request .app ) as user_session :
464- project_users = {
465- s .user_id
466- for s in await user_session .find_users_of_resource (
467- request .app , PROJECT_ID_KEY , f"{ path_params .project_id } "
468- )
469- }
470- # that project is still in use
471- if req_ctx .user_id in project_users :
472- raise web .HTTPForbidden (
473- reason = "Project is still open in another tab/browser."
474- "It cannot be deleted until it is closed."
475- )
476- if project_users :
477- other_user_names = {
478- f"{ await get_user_fullname (request .app , user_id = uid )} "
479- for uid in project_users
480- }
481- raise web .HTTPForbidden (
482- reason = f"Project is open by { other_user_names } . "
483- "It cannot be deleted until the project is closed."
484- )
485-
486- project_locked_state : ProjectLocked | None
487- if project_locked_state := await get_project_locked_state (
488- get_redis_lock_manager_client_sdk (request .app ),
489- project_uuid = path_params .project_id ,
490- ):
491- raise web .HTTPConflict (
492- reason = f"Project { path_params .project_id } is locked: { project_locked_state = } "
404+ await projects_service .get_project_for_user (
405+ request .app ,
406+ project_uuid = f"{ path_params .project_id } " ,
407+ user_id = req_ctx .user_id ,
408+ )
409+ project_users : set [int ] = set ()
410+ with managed_resource (req_ctx .user_id , None , request .app ) as user_session :
411+ project_users = {
412+ s .user_id
413+ for s in await user_session .find_users_of_resource (
414+ request .app , PROJECT_ID_KEY , f"{ path_params .project_id } "
493415 )
416+ }
417+ # that project is still in use
418+ if req_ctx .user_id in project_users :
419+ raise web .HTTPForbidden (
420+ reason = "Project is still open in another tab/browser."
421+ "It cannot be deleted until it is closed."
422+ )
423+ if project_users :
424+ other_user_names = {
425+ f"{ await get_user_fullname (request .app , user_id = uid )} "
426+ for uid in project_users
427+ }
428+ raise web .HTTPForbidden (
429+ reason = f"Project is open by { other_user_names } . "
430+ "It cannot be deleted until the project is closed."
431+ )
494432
495- await projects_service . submit_delete_project_task (
496- request . app ,
497- project_uuid = path_params . project_id ,
498- user_id = req_ctx . user_id ,
499- simcore_user_agent = request . headers . get (
500- X_SIMCORE_USER_AGENT , UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
501- ),
433+ project_locked_state : ProjectLocked | None
434+ if project_locked_state := await get_project_locked_state (
435+ get_redis_lock_manager_client_sdk ( request . app ) ,
436+ project_uuid = path_params . project_id ,
437+ ):
438+ raise web . HTTPConflict (
439+ reason = f"Project { path_params . project_id } is locked: { project_locked_state = } "
502440 )
503441
504- except ProjectInvalidRightsError as err :
505- raise web .HTTPForbidden (
506- reason = "You do not have sufficient rights to delete this project"
507- ) from err
508- except ProjectNotFoundError as err :
509- raise web .HTTPNotFound (
510- reason = f"Project { path_params .project_id } not found"
511- ) from err
512- except ProjectDeleteError as err :
513- raise web .HTTPConflict (reason = f"{ err } " ) from err
442+ await projects_service .submit_delete_project_task (
443+ request .app ,
444+ project_uuid = path_params .project_id ,
445+ user_id = req_ctx .user_id ,
446+ simcore_user_agent = request .headers .get (
447+ X_SIMCORE_USER_AGENT , UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
448+ ),
449+ )
514450
515451 return web .json_response (status = status .HTTP_204_NO_CONTENT )
516452
@@ -526,7 +462,7 @@ async def delete_project(request: web.Request):
526462@login_required
527463@permission_required ("project.create" )
528464@permission_required ("services.pipeline.*" ) # due to update_pipeline_db
529- @_handle_projects_exceptions
465+ @handle_plugin_requests_exceptions
530466async def clone_project (request : web .Request ):
531467 req_ctx = RequestContext .model_validate (request )
532468 path_params = parse_request_path_parameters_as (ProjectPathParams , request )
0 commit comments