From 99104eb7238337a5dcbee8373e77911a27f214fd Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:07:21 +0100 Subject: [PATCH 01/52] exc handlers --- .../workspaces/_exceptions_handlers.py | 34 +++++++++++++++++++ .../workspaces/_groups_handlers.py | 29 +++++----------- 2 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py new file mode 100644 index 00000000000..88319c44d2c --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py @@ -0,0 +1,34 @@ +import logging + +from servicelib.aiohttp import status + +from ..exceptions_handlers import ( + ExceptionToHttpErrorMap, + HttpErrorInfo, + create_exception_handlers_decorator, +) +from .errors import ( + WorkspaceAccessForbiddenError, + WorkspaceGroupNotFoundError, + WorkspacesValueError, +) + +_logger = logging.getLogger(__name__) + + +_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { + WorkspaceGroupNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Workspace {workspace_id} group {group_id} not found.", + ), + WorkspaceAccessForbiddenError: HttpErrorInfo( + status.HTTP_403_FORBIDDEN, + "Does not have access to this workspace", + ), +} + + +handle_plugin_requests_exceptions = create_exception_handlers_decorator( + exceptions_catch=(WorkspacesValueError), + exc_to_status_map=_TO_HTTP_ERROR_MAP, +) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py index c75ab891ef6..22e40a9b6e8 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py @@ -2,7 +2,6 @@ """ -import functools import logging from aiohttp import web @@ -14,7 +13,7 @@ parse_request_body_as, parse_request_path_parameters_as, ) -from servicelib.aiohttp.typing_extension import Handler +from servicelib.request_keys import RQT_USERID_KEY from .._meta import api_version_prefix as VTAG from ..login.decorators import login_required @@ -22,26 +21,16 @@ from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _groups_api +from ._exceptions_handlers import handle_plugin_requests_exceptions from ._groups_api import WorkspaceGroupGet from ._workspaces_handlers import WorkspacesPathParams -from .errors import WorkspaceAccessForbiddenError, WorkspaceGroupNotFoundError _logger = logging.getLogger(__name__) -def _handle_workspaces_groups_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except WorkspaceGroupNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except WorkspaceAccessForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper +class _RequestContext(BaseModel): + user_id: UserID = Field(..., alias=RQT_USERID_KEY) # type: ignore[literal-required] + product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[literal-required] # @@ -74,7 +63,7 @@ class Config: ) @login_required @permission_required("workspaces.*") -@_handle_workspaces_groups_exceptions +@handle_plugin_requests_exceptions async def create_workspace_group(request: web.Request): req_ctx = RequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) @@ -97,7 +86,7 @@ async def create_workspace_group(request: web.Request): @routes.get(f"/{VTAG}/workspaces/{{workspace_id}}/groups", name="list_workspace_groups") @login_required @permission_required("workspaces.*") -@_handle_workspaces_groups_exceptions +@handle_plugin_requests_exceptions async def list_workspace_groups(request: web.Request): req_ctx = RequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) @@ -120,7 +109,7 @@ async def list_workspace_groups(request: web.Request): ) @login_required @permission_required("workspaces.*") -@_handle_workspaces_groups_exceptions +@handle_plugin_requests_exceptions async def replace_workspace_group(request: web.Request): req_ctx = RequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) @@ -144,7 +133,7 @@ async def replace_workspace_group(request: web.Request): ) @login_required @permission_required("workspaces.*") -@_handle_workspaces_groups_exceptions +@handle_plugin_requests_exceptions async def delete_workspace_group(request: web.Request): req_ctx = RequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) From 4b0071d62e03840962da1f1dfb3df41fb12b60d3 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:12:14 +0100 Subject: [PATCH 02/52] exc handlers in workspaces --- .../workspaces/_exceptions_handlers.py | 5 +++ .../workspaces/_workspaces_handlers.py | 33 ++++--------------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py index 88319c44d2c..aa59bd6a036 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py @@ -10,6 +10,7 @@ from .errors import ( WorkspaceAccessForbiddenError, WorkspaceGroupNotFoundError, + WorkspaceNotFoundError, WorkspacesValueError, ) @@ -25,6 +26,10 @@ status.HTTP_403_FORBIDDEN, "Does not have access to this workspace", ), + WorkspaceNotFoundError: HttpErrorInfo( + status.HTTP_404_NOT_FOUND, + "Workspace not found. {reason}", + ), } diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index f4e4b6b8088..3b553945c78 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -1,4 +1,3 @@ -import functools import logging from aiohttp import web @@ -26,7 +25,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.request_keys import RQT_USERID_KEY from servicelib.rest_constants import RESPONSE_MODEL_POLICY @@ -37,30 +35,11 @@ from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _workspaces_api -from .errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError +from ._exceptions_handlers import handle_plugin_requests_exceptions _logger = logging.getLogger(__name__) -def handle_workspaces_exceptions(handler: Handler): - @functools.wraps(handler) - async def wrapper(request: web.Request) -> web.StreamResponse: - try: - return await handler(request) - - except WorkspaceNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc - - except WorkspaceAccessForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc - - return wrapper - - -# -# workspaces COLLECTION ------------------------- -# - routes = web.RouteTableDef() @@ -95,7 +74,7 @@ class WorkspacesListQueryParams( @routes.post(f"/{VTAG}/workspaces", name="create_workspace") @login_required @permission_required("workspaces.*") -@handle_workspaces_exceptions +@handle_plugin_requests_exceptions async def create_workspace(request: web.Request): req_ctx = WorkspacesRequestContext.parse_obj(request) body_params = await parse_request_body_as(CreateWorkspaceBodyParams, request) @@ -115,7 +94,7 @@ async def create_workspace(request: web.Request): @routes.get(f"/{VTAG}/workspaces", name="list_workspaces") @login_required @permission_required("workspaces.*") -@handle_workspaces_exceptions +@handle_plugin_requests_exceptions async def list_workspaces(request: web.Request): req_ctx = WorkspacesRequestContext.parse_obj(request) query_params: WorkspacesListQueryParams = parse_request_query_parameters_as( @@ -149,7 +128,7 @@ async def list_workspaces(request: web.Request): @routes.get(f"/{VTAG}/workspaces/{{workspace_id}}", name="get_workspace") @login_required @permission_required("workspaces.*") -@handle_workspaces_exceptions +@handle_plugin_requests_exceptions async def get_workspace(request: web.Request): req_ctx = WorkspacesRequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) @@ -170,7 +149,7 @@ async def get_workspace(request: web.Request): ) @login_required @permission_required("workspaces.*") -@handle_workspaces_exceptions +@handle_plugin_requests_exceptions async def replace_workspace(request: web.Request): req_ctx = WorkspacesRequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) @@ -194,7 +173,7 @@ async def replace_workspace(request: web.Request): ) @login_required @permission_required("workspaces.*") -@handle_workspaces_exceptions +@handle_plugin_requests_exceptions async def delete_workspace(request: web.Request): req_ctx = WorkspacesRequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) From 3586430074b63d3c07545d286db993caf56986f4 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:30:04 +0100 Subject: [PATCH 03/52] separates models --- .../workspaces/_groups_handlers.py | 63 ++++++------------- .../workspaces/_models.py | 63 +++++++++++++++++++ .../workspaces/_workspaces_handlers.py | 49 +++------------ 3 files changed, 90 insertions(+), 85 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/workspaces/_models.py diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py index 22e40a9b6e8..8f885a5833e 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py @@ -1,38 +1,29 @@ -""" Handlers for project comments operations - -""" - import logging from aiohttp import web -from models_library.users import GroupID -from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, Extra from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, ) -from servicelib.request_keys import RQT_USERID_KEY from .._meta import api_version_prefix as VTAG from ..login.decorators import login_required -from ..models import RequestContext from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _groups_api from ._exceptions_handlers import handle_plugin_requests_exceptions from ._groups_api import WorkspaceGroupGet -from ._workspaces_handlers import WorkspacesPathParams +from ._models import ( + WorkspacesGroupsBodyParams, + WorkspacesGroupsPathParams, + WorkspacesPathParams, + WorkspacesRequestContext, +) _logger = logging.getLogger(__name__) -class _RequestContext(BaseModel): - user_id: UserID = Field(..., alias=RQT_USERID_KEY) # type: ignore[literal-required] - product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[literal-required] - - # # workspaces groups COLLECTION ------------------------- # @@ -40,23 +31,6 @@ class _RequestContext(BaseModel): routes = web.RouteTableDef() -class _WorkspacesGroupsPathParams(BaseModel): - workspace_id: WorkspaceID - group_id: GroupID - - class Config: - extra = Extra.forbid - - -class _WorkspacesGroupsBodyParams(BaseModel): - read: bool - write: bool - delete: bool - - class Config: - extra = Extra.forbid - - @routes.post( f"/{VTAG}/workspaces/{{workspace_id}}/groups/{{group_id}}", name="create_workspace_group", @@ -65,9 +39,9 @@ class Config: @permission_required("workspaces.*") @handle_plugin_requests_exceptions async def create_workspace_group(request: web.Request): - req_ctx = RequestContext.parse_obj(request) - path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) - body_params = await parse_request_body_as(_WorkspacesGroupsBodyParams, request) + req_ctx = WorkspacesRequestContext.parse_obj(request) + path_params = parse_request_path_parameters_as(WorkspacesGroupsPathParams, request) + body_params = await parse_request_body_as(WorkspacesGroupsBodyParams, request) workspace_groups: WorkspaceGroupGet = await _groups_api.create_workspace_group( request.app, @@ -88,10 +62,10 @@ async def create_workspace_group(request: web.Request): @permission_required("workspaces.*") @handle_plugin_requests_exceptions async def list_workspace_groups(request: web.Request): - req_ctx = RequestContext.parse_obj(request) + req_ctx = WorkspacesRequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) - workspaces: list[ + workspaces_groups: list[ WorkspaceGroupGet ] = await _groups_api.list_workspace_groups_by_user_and_workspace( request.app, @@ -100,7 +74,7 @@ async def list_workspace_groups(request: web.Request): product_name=req_ctx.product_name, ) - return envelope_json_response(workspaces, web.HTTPOk) + return envelope_json_response(workspaces_groups) @routes.put( @@ -111,11 +85,11 @@ async def list_workspace_groups(request: web.Request): @permission_required("workspaces.*") @handle_plugin_requests_exceptions async def replace_workspace_group(request: web.Request): - req_ctx = RequestContext.parse_obj(request) - path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) - body_params = await parse_request_body_as(_WorkspacesGroupsBodyParams, request) + req_ctx = WorkspacesRequestContext.parse_obj(request) + path_params = parse_request_path_parameters_as(WorkspacesGroupsPathParams, request) + body_params = await parse_request_body_as(WorkspacesGroupsBodyParams, request) - return await _groups_api.update_workspace_group( + workspace_group = await _groups_api.update_workspace_group( app=request.app, user_id=req_ctx.user_id, workspace_id=path_params.workspace_id, @@ -125,6 +99,7 @@ async def replace_workspace_group(request: web.Request): delete=body_params.delete, product_name=req_ctx.product_name, ) + return envelope_json_response(workspace_group) @routes.delete( @@ -135,8 +110,8 @@ async def replace_workspace_group(request: web.Request): @permission_required("workspaces.*") @handle_plugin_requests_exceptions async def delete_workspace_group(request: web.Request): - req_ctx = RequestContext.parse_obj(request) - path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request) + req_ctx = WorkspacesRequestContext.parse_obj(request) + path_params = parse_request_path_parameters_as(WorkspacesGroupsPathParams, request) await _groups_api.delete_workspace_group( app=request.app, diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_models.py b/services/web/server/src/simcore_service_webserver/workspaces/_models.py new file mode 100644 index 00000000000..576fb5bcf9f --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/workspaces/_models.py @@ -0,0 +1,63 @@ +import logging + +from models_library.basic_types import IDStr +from models_library.rest_base import RequestParameters, StrictRequestParameters +from models_library.rest_ordering import ( + OrderBy, + OrderDirection, + create_ordering_query_model_classes, +) +from models_library.rest_pagination import PageQueryParameters +from models_library.users import GroupID, UserID +from models_library.workspaces import WorkspaceID +from pydantic import BaseModel, Extra, Field +from servicelib.request_keys import RQT_USERID_KEY + +from .._constants import RQ_PRODUCT_KEY + +_logger = logging.getLogger(__name__) + + +class WorkspacesRequestContext(RequestParameters): + user_id: UserID = Field(..., alias=RQT_USERID_KEY) # type: ignore[literal-required] + product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[literal-required] + + +class WorkspacesPathParams(StrictRequestParameters): + workspace_id: WorkspaceID + + +WorkspacesListOrderQueryParams: type[ + RequestParameters +] = create_ordering_query_model_classes( + ordering_fields={ + "modified_at", + "name", + }, + default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC), + ordering_fields_api_to_column_map={"modified_at": "modified"}, +) + + +class WorkspacesListQueryParams( + PageQueryParameters, + WorkspacesListOrderQueryParams, # type: ignore[misc, valid-type] +): + ... + + +class WorkspacesGroupsPathParams(BaseModel): + workspace_id: WorkspaceID + group_id: GroupID + + class Config: + extra = Extra.forbid + + +class WorkspacesGroupsBodyParams(BaseModel): + read: bool + write: bool + delete: bool + + class Config: + extra = Extra.forbid diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index 3b553945c78..2e650647c4e 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -7,18 +7,10 @@ WorkspaceGet, WorkspaceGetPage, ) -from models_library.basic_types import IDStr -from models_library.rest_base import RequestParameters, StrictRequestParameters -from models_library.rest_ordering import ( - OrderBy, - OrderDirection, - create_ordering_query_model_classes, -) -from models_library.rest_pagination import Page, PageQueryParameters +from models_library.rest_ordering import OrderBy +from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data -from models_library.users import UserID -from models_library.workspaces import WorkspaceID -from pydantic import Field, parse_obj_as +from pydantic import parse_obj_as from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -26,16 +18,19 @@ parse_request_query_parameters_as, ) from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON -from servicelib.request_keys import RQT_USERID_KEY from servicelib.rest_constants import RESPONSE_MODEL_POLICY -from .._constants import RQ_PRODUCT_KEY from .._meta import API_VTAG as VTAG from ..login.decorators import login_required from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response from . import _workspaces_api from ._exceptions_handlers import handle_plugin_requests_exceptions +from ._models import ( + WorkspacesListQueryParams, + WorkspacesPathParams, + WorkspacesRequestContext, +) _logger = logging.getLogger(__name__) @@ -43,34 +38,6 @@ routes = web.RouteTableDef() -class WorkspacesRequestContext(RequestParameters): - user_id: UserID = Field(..., alias=RQT_USERID_KEY) # type: ignore[literal-required] - product_name: str = Field(..., alias=RQ_PRODUCT_KEY) # type: ignore[literal-required] - - -class WorkspacesPathParams(StrictRequestParameters): - workspace_id: WorkspaceID - - -WorkspacesListOrderQueryParams: type[ - RequestParameters -] = create_ordering_query_model_classes( - ordering_fields={ - "modified_at", - "name", - }, - default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC), - ordering_fields_api_to_column_map={"modified_at": "modified"}, -) - - -class WorkspacesListQueryParams( - PageQueryParameters, - WorkspacesListOrderQueryParams, # type: ignore[misc, valid-type] -): - ... - - @routes.post(f"/{VTAG}/workspaces", name="create_workspace") @login_required @permission_required("workspaces.*") From bf3b67c41209bba1966c47e0516303bd2ab86858 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:37:21 +0100 Subject: [PATCH 04/52] updates oas builder --- api/specs/web-server/_trash.py | 36 +++++++++++++++++++++++++- api/specs/web-server/_workspaces.py | 39 ++++++++++++++++++----------- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index 9aa23b8b288..f66031a567d 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -17,6 +17,7 @@ from simcore_service_webserver.projects._trash_handlers import ( RemoveQueryParams as RemoveQueryParams_duplicated, ) +from simcore_service_webserver.workspaces._models import WorkspacesPathParams router = APIRouter( prefix=f"/{API_VTAG}", @@ -75,7 +76,7 @@ def untrash_project( responses={ status.HTTP_404_NOT_FOUND: {"description": "Not such a folder"}, status.HTTP_409_CONFLICT: { - "description": "One or more projects is in use and cannot be trashed" + "description": "One or more projects are in use and cannot be trashed" }, status.HTTP_503_SERVICE_UNAVAILABLE: {"description": "Trash service error"}, }, @@ -96,3 +97,36 @@ def untrash_folder( _path: Annotated[FoldersPathParams, Depends()], ): ... + + +_extra_tags = ["workspaces"] + + +@router.post( + "/workspace/{workspace_id}:trash", + tags=_extra_tags, + status_code=status.HTTP_204_NO_CONTENT, + responses={ + status.HTTP_404_NOT_FOUND: {"description": "Not such a workspace"}, + status.HTTP_409_CONFLICT: { + "description": "One or more projects are in use and cannot be trashed" + }, + status.HTTP_503_SERVICE_UNAVAILABLE: {"description": "Trash service error"}, + }, +) +def trash_workspace( + _p: Annotated[WorkspacesPathParams, Depends()], + _q: Annotated[RemoveQueryParams_duplicated, Depends()], +): + ... + + +@router.post( + "/workspace/{workspace_id}:untrash", + tags=_extra_tags, + status_code=status.HTTP_204_NO_CONTENT, +) +def untrash_workspace( + _p: Annotated[WorkspacesPathParams, Depends()], +): + ... diff --git a/api/specs/web-server/_workspaces.py b/api/specs/web-server/_workspaces.py index e3f1b4ebc5c..cf27277d138 100644 --- a/api/specs/web-server/_workspaces.py +++ b/api/specs/web-server/_workspaces.py @@ -17,12 +17,12 @@ WorkspaceGet, ) from models_library.generics import Envelope -from models_library.workspaces import WorkspaceID from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.workspaces._groups_api import WorkspaceGroupGet -from simcore_service_webserver.workspaces._groups_handlers import ( - _WorkspacesGroupsBodyParams, - _WorkspacesGroupsPathParams, +from simcore_service_webserver.workspaces._models import ( + WorkspacesGroupsBodyParams, + WorkspacesGroupsPathParams, + WorkspacesPathParams, ) router = APIRouter( @@ -40,7 +40,9 @@ response_model=Envelope[WorkspaceGet], status_code=status.HTTP_201_CREATED, ) -async def create_workspace(_body: CreateWorkspaceBodyParams): +async def create_workspace( + _b: CreateWorkspaceBodyParams, +): ... @@ -56,7 +58,9 @@ async def list_workspaces(): "/workspaces/{workspace_id}", response_model=Envelope[WorkspaceGet], ) -async def get_workspace(workspace_id: WorkspaceID): +async def get_workspace( + _p: Annotated[WorkspacesPathParams, Depends()], +): ... @@ -64,7 +68,10 @@ async def get_workspace(workspace_id: WorkspaceID): "/workspaces/{workspace_id}", response_model=Envelope[WorkspaceGet], ) -async def replace_workspace(workspace_id: WorkspaceID, _body: PutWorkspaceBodyParams): +async def replace_workspace( + _p: Annotated[WorkspacesPathParams, Depends()], + _b: PutWorkspaceBodyParams, +): ... @@ -72,7 +79,9 @@ async def replace_workspace(workspace_id: WorkspaceID, _body: PutWorkspaceBodyPa "/workspaces/{workspace_id}", status_code=status.HTTP_204_NO_CONTENT, ) -async def delete_workspace(workspace_id: WorkspaceID): +async def delete_workspace( + _p: Annotated[WorkspacesPathParams, Depends()], +): ... @@ -87,8 +96,8 @@ async def delete_workspace(workspace_id: WorkspaceID): tags=_extra_tags, ) async def create_workspace_group( - _path_parms: Annotated[_WorkspacesGroupsPathParams, Depends()], - _body: _WorkspacesGroupsBodyParams, + _p: Annotated[WorkspacesGroupsPathParams, Depends()], + _b: WorkspacesGroupsBodyParams, ): ... @@ -98,7 +107,9 @@ async def create_workspace_group( response_model=Envelope[list[WorkspaceGroupGet]], tags=_extra_tags, ) -async def list_workspace_groups(workspace_id: WorkspaceID): +async def list_workspace_groups( + _p: Annotated[WorkspacesPathParams, Depends()], +): ... @@ -108,8 +119,8 @@ async def list_workspace_groups(workspace_id: WorkspaceID): tags=_extra_tags, ) async def replace_workspace_group( - _path_parms: Annotated[_WorkspacesGroupsPathParams, Depends()], - _body: _WorkspacesGroupsBodyParams, + _p: Annotated[WorkspacesGroupsPathParams, Depends()], + _b: WorkspacesGroupsBodyParams, ): ... @@ -120,6 +131,6 @@ async def replace_workspace_group( tags=_extra_tags, ) async def delete_workspace_group( - _path_parms: Annotated[_WorkspacesGroupsPathParams, Depends()] + _p: Annotated[WorkspacesGroupsPathParams, Depends()], ): ... From 4886caa6a30a9023621053f1da59bc25c2a136e2 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:38:03 +0100 Subject: [PATCH 05/52] updates OAS --- .../api/v0/openapi.yaml | 93 ++++++++++++++----- 1 file changed, 72 insertions(+), 21 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 df35af2db92..153c614e957 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 @@ -5500,7 +5500,7 @@ paths: '404': description: Not such a folder '409': - description: One or more projects is in use and cannot be trashed + description: One or more projects are in use and cannot be trashed '503': description: Trash service error /v0/folders/{folder_id}:untrash: @@ -5522,6 +5522,57 @@ paths: responses: '204': description: Successful Response + /v0/workspace/{workspace_id}:trash: + post: + tags: + - trash + - workspaces + summary: Trash Workspace + operationId: trash_workspace + parameters: + - required: true + schema: + title: Workspace Id + exclusiveMinimum: true + type: integer + minimum: 0 + name: workspace_id + in: path + - required: false + schema: + title: Force + type: boolean + default: false + name: force + in: query + responses: + '204': + description: Successful Response + '404': + description: Not such a workspace + '409': + description: One or more projects are in use and cannot be trashed + '503': + description: Trash service error + /v0/workspace/{workspace_id}:untrash: + post: + tags: + - trash + - workspaces + summary: Untrash Workspace + operationId: untrash_workspace + parameters: + - required: true + schema: + title: Workspace Id + exclusiveMinimum: true + type: integer + minimum: 0 + name: workspace_id + in: path + responses: + '204': + description: Successful Response /v0/repos/projects: get: tags: @@ -5869,7 +5920,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/_WorkspacesGroupsBodyParams' + $ref: '#/components/schemas/WorkspacesGroupsBodyParams' required: true responses: '200': @@ -5905,7 +5956,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/_WorkspacesGroupsBodyParams' + $ref: '#/components/schemas/WorkspacesGroupsBodyParams' required: true responses: '201': @@ -13168,6 +13219,24 @@ components: title: Modified type: string format: date-time + WorkspacesGroupsBodyParams: + title: WorkspacesGroupsBodyParams + required: + - read + - write + - delete + type: object + properties: + read: + title: Read + type: boolean + write: + title: Write + type: boolean + delete: + title: Delete + type: boolean + additionalProperties: false _ComputationStarted: title: _ComputationStarted required: @@ -13280,24 +13349,6 @@ components: title: Delete type: boolean additionalProperties: false - _WorkspacesGroupsBodyParams: - title: _WorkspacesGroupsBodyParams - required: - - read - - write - - delete - type: object - properties: - read: - title: Read - type: boolean - write: - title: Write - type: boolean - delete: - title: Delete - type: boolean - additionalProperties: false models_library__access_rights__AccessRights: title: AccessRights required: From 39b8d4b5c860d210e12e0ee4c5c6fdaf2a94331f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:38:09 +0100 Subject: [PATCH 06/52] =?UTF-8?q?services/webserver=20api=20version:=200.4?= =?UTF-8?q?5.0=20=E2=86=92=200.46.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/web/server/VERSION | 2 +- services/web/server/setup.cfg | 2 +- .../server/src/simcore_service_webserver/api/v0/openapi.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/server/VERSION b/services/web/server/VERSION index bcce5d06b8a..301092317fe 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.45.0 +0.46.0 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 2b54478220b..0c5bbbcb6b3 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.45.0 +current_version = 0.46.0 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False 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 153c614e957..f7c96e11a40 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 @@ -2,7 +2,7 @@ openapi: 3.0.2 info: title: simcore-service-webserver description: Main service with an interface (http-API & websockets) to the web front-end - version: 0.45.0 + version: 0.46.0 servers: - url: '' description: webserver From f94ab75f7da50f1365755faf8ffc69a6c973732c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:43:47 +0100 Subject: [PATCH 07/52] drafts trash section --- .../workspaces/_trash_api.py | 69 +++++++++++++++++++ .../workspaces/_trash_handlers.py | 66 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py create mode 100644 services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py new file mode 100644 index 00000000000..c07a35a3091 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py @@ -0,0 +1,69 @@ +import logging + +import arrow +from aiohttp import web +from models_library.products import ProductName +from models_library.projects import ProjectID +from models_library.users import UserID +from models_library.workspaces import WorkspaceID + +from ..projects._trash_api import trash_project, untrash_project + +_logger = logging.getLogger(__name__) + + +async def trash_workspace( + app: web.Application, + *, + product_name: ProductName, + user_id: UserID, + workspace_id: WorkspaceID, + force_stop_first: bool, +): + # TODO: Check + + # Trash + trashed_at = arrow.utcnow().datetime + + _logger.debug( + "TODO: Unit of work for all workspaces and projects and fails if force_stop_first=%s is False", + force_stop_first, + ) + + # 1. TODO: Trash workspace + + # 2. Trash all child folders + + # 2. Trash all child projects that I am an owner + child_projects: list[ProjectID] = [] + + for project_id in child_projects: + await trash_project( + app, + product_name=product_name, + user_id=user_id, + project_id=project_id, + force_stop_first=force_stop_first, + explicit=False, + ) + + +async def untrash_workspace( + app: web.Application, + *, + product_name: ProductName, + user_id: UserID, + workspace_id: WorkspaceID, +): + # TODO: Check + + # 3. UNtrash + + # 3.1 UNtrash workspace and children + + # 3.2 UNtrash all child projects that I am an owner + child_projects: list[ProjectID] = [] + for project_id in child_projects: + await untrash_project( + app, product_name=product_name, user_id=user_id, project_id=project_id + ) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py new file mode 100644 index 00000000000..dcd45ea8d89 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py @@ -0,0 +1,66 @@ +import logging + +from aiohttp import web +from servicelib.aiohttp import status +from servicelib.aiohttp.requests_validation import ( + parse_request_path_parameters_as, + parse_request_query_parameters_as, +) + +from .._meta import API_VTAG as VTAG +from ..application_settings_utils import requires_dev_feature_enabled +from ..login.decorators import get_user_id, login_required +from ..products.api import get_product_name +from ..security.decorators import permission_required +from . import _trash_api +from ._exceptions_handlers import handle_plugin_requests_exceptions +from ._models import RemoveQueryParams, WorkspacesPathParams + +_logger = logging.getLogger(__name__) + + +routes = web.RouteTableDef() + + +@routes.post(f"/{VTAG}/workspaces/{{workspace_id}}:trash", name="trash_workspace") +@requires_dev_feature_enabled +@login_required +@permission_required("workspace.delete") +@handle_plugin_requests_exceptions +async def trash_workspace(request: web.Request): + user_id = get_user_id(request) + product_name = get_product_name(request) + path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) + query_params: RemoveQueryParams = parse_request_query_parameters_as( + RemoveQueryParams, request + ) + + await _trash_api.trash_workspace( + request.app, + product_name=product_name, + user_id=user_id, + workspace_id=path_params.workspace_id, + force_stop_first=query_params.force, + ) + + return web.json_response(status=status.HTTP_204_NO_CONTENT) + + +@routes.post(f"/{VTAG}/workspaces/{{workspace_id}}:untrash", name="untrash_workspace") +@requires_dev_feature_enabled +@login_required +@permission_required("workspace.delete") +@handle_plugin_requests_exceptions +async def untrash_workspace(request: web.Request): + user_id = get_user_id(request) + product_name = get_product_name(request) + path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) + + await _trash_api.untrash_workspace( + request.app, + product_name=product_name, + user_id=user_id, + workspace_id=path_params.workspace_id, + ) + + return web.json_response(status=status.HTTP_204_NO_CONTENT) From cc35618852325fbd0b3904711ce91a19ff91ad11 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:41:47 +0100 Subject: [PATCH 08/52] updates oas --- api/specs/web-server/_workspaces.py | 26 +++++++++++++++---- .../workspaces/_models.py | 9 +++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/api/specs/web-server/_workspaces.py b/api/specs/web-server/_workspaces.py index cf27277d138..5e0180024f1 100644 --- a/api/specs/web-server/_workspaces.py +++ b/api/specs/web-server/_workspaces.py @@ -6,22 +6,24 @@ # pylint: disable=unused-variable # pylint: disable=too-many-arguments - from enum import Enum from typing import Annotated -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, Query, status from models_library.api_schemas_webserver.workspaces import ( CreateWorkspaceBodyParams, PutWorkspaceBodyParams, WorkspaceGet, ) from models_library.generics import Envelope +from pydantic import Json from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.workspaces._groups_api import WorkspaceGroupGet from simcore_service_webserver.workspaces._models import ( + WorkspacesFilters, WorkspacesGroupsBodyParams, WorkspacesGroupsPathParams, + WorkspacesListWithJsonStrQueryParams, WorkspacesPathParams, ) @@ -32,8 +34,6 @@ ], ) -### Workspaces - @router.post( "/workspaces", @@ -50,7 +50,23 @@ async def create_workspace( "/workspaces", response_model=Envelope[list[WorkspaceGet]], ) -async def list_workspaces(): +async def list_workspaces( + order_by: Annotated[ + Json, + Query( + description=WorkspacesListWithJsonStrQueryParams.__fields__[ + "order_by" + ].field_info.description, + example=WorkspacesListWithJsonStrQueryParams.__fields__[ + "order_by" + ].field_info.extra.get("example"), + ), + ] = WorkspacesListWithJsonStrQueryParams.__fields__["order_by"].field_info.default, + filters: Annotated[ + Json | None, + Query(description=WorkspacesFilters.schema_json(indent=1)), + ] = None, +): ... diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_models.py b/services/web/server/src/simcore_service_webserver/workspaces/_models.py index 576fb5bcf9f..3a366ad7cba 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_models.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_models.py @@ -2,6 +2,7 @@ from models_library.basic_types import IDStr from models_library.rest_base import RequestParameters, StrictRequestParameters +from models_library.rest_filters import Filters, FiltersQueryParameters from models_library.rest_ordering import ( OrderBy, OrderDirection, @@ -39,8 +40,16 @@ class WorkspacesPathParams(StrictRequestParameters): ) +class WorkspacesFilters(Filters): + trashed: bool | None = Field( + default=False, + description="Set to true to list trashed, false to list non-trashed (default), None to list all", + ) + + class WorkspacesListQueryParams( PageQueryParameters, + FiltersQueryParameters[WorkspacesFilters], WorkspacesListOrderQueryParams, # type: ignore[misc, valid-type] ): ... From 0e5ddb0095705436da7bc216b6e40615a8089f11 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:13:54 +0100 Subject: [PATCH 09/52] minor --- .../models-library/src/models_library/workspaces.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/models-library/src/models_library/workspaces.py b/packages/models-library/src/models_library/workspaces.py index e5b816623fe..32854f1297b 100644 --- a/packages/models-library/src/models_library/workspaces.py +++ b/packages/models-library/src/models_library/workspaces.py @@ -26,13 +26,11 @@ class WorkspaceQuery(BaseModel): def validate_workspace_id(cls, value, values): scope = values.get("workspace_scope") if scope == WorkspaceScope.SHARED and value is None: - raise ValueError( - "workspace_id must be provided when workspace_scope is SHARED." - ) + msg = f"workspace_id must be provided when workspace_scope is SHARED. Got {scope=}, {value=}" + raise ValueError(msg) if scope != WorkspaceScope.SHARED and value is not None: - raise ValueError( - "workspace_id should be None when workspace_scope is not SHARED." - ) + msg = f"workspace_id should be None when workspace_scope is not SHARED. Got {scope=}, {value=}" + raise ValueError(msg) return value @@ -58,6 +56,7 @@ class WorkspaceDB(BaseModel): ..., description="Timestamp of last modification", ) + trashed_at: datetime | None class Config: orm_mode = True From b22adaf6fec073ce87dfd2271259df460afcf851 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:15:18 +0100 Subject: [PATCH 10/52] adds db --- .../models/_common.py | 25 +++++++++++++++++++ .../models/workspaces.py | 11 +++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/_common.py b/packages/postgres-database/src/simcore_postgres_database/models/_common.py index c3f671d244a..8aaa1e08124 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/_common.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/_common.py @@ -58,6 +58,31 @@ def column_modified_by_user( ) +def column_trashed_datetime(resource_name: str) -> sa.Column: + return sa.Column( + "trashed", + sa.DateTime(timezone=True), + nullable=True, + comment=f"The date and time when the {resource_name} was marked as trashed." + f"Null if the {resource_name} has not been trashed [default].", + ) + + +def column_trashed_by_user(resource_name: str, users_table: sa.Table) -> sa.Column: + return sa.Column( + "trashed_by", + sa.BigInteger, + sa.ForeignKey( + users_table.c.id, + onupdate="CASCADE", + ondelete="SET NULL", + ), + nullable=True, + comment=f"The user who marked the {resource_name} as trashed " + "or NULL if unknown or unmarked.", + ) + + _TRIGGER_NAME: Final[str] = "auto_update_modified_timestamp" diff --git a/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py b/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py index 998c7676761..7f1b7963929 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py @@ -1,7 +1,13 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import ( + column_created_datetime, + column_modified_datetime, + column_trashed_by_user, + column_trashed_datetime, +) from .base import metadata +from .users import users workspaces = sa.Table( "workspaces", @@ -48,8 +54,11 @@ ), column_created_datetime(timezone=True), column_modified_datetime(timezone=True), + column_trashed_datetime("workspace"), + column_trashed_by_user("workspace", users_table=users), ) + # ------------------------ TRIGGERS new_workspace_trigger = sa.DDL( """ From 3e788bde03a213b76baedb2666924f2b65840bcb Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:18:07 +0100 Subject: [PATCH 11/52] db models --- .../models-library/src/models_library/workspaces.py | 5 +++-- .../workspaces/_workspaces_db.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/models-library/src/models_library/workspaces.py b/packages/models-library/src/models_library/workspaces.py index 32854f1297b..418397c7dce 100644 --- a/packages/models-library/src/models_library/workspaces.py +++ b/packages/models-library/src/models_library/workspaces.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Field, PositiveInt, validator from .access_rights import AccessRights -from .users import GroupID +from .users import GroupID, UserID from .utils.enums import StrAutoEnum WorkspaceID: TypeAlias = PositiveInt @@ -56,7 +56,8 @@ class WorkspaceDB(BaseModel): ..., description="Timestamp of last modification", ) - trashed_at: datetime | None + trashed: datetime | None + trashed_by: UserID | None class Config: orm_mode = True diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index fa0ab9dbab6..7c1366e993e 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -46,6 +46,8 @@ workspaces.c.thumbnail, workspaces.c.created, workspaces.c.modified, + workspaces.c.trashed, + workspaces.c.trashed_by, ) @@ -105,6 +107,7 @@ async def list_workspaces_for_user( *, user_id: UserID, product_name: ProductName, + trashed: bool | None, offset: NonNegativeInt, limit: NonNegativeInt, order_by: OrderBy, @@ -125,6 +128,13 @@ async def list_workspaces_for_user( .where(workspaces.c.product_name == product_name) ) + if trashed is not None: + base_query = base_query.where( + workspaces.c.trashed_at.is_not(None) + if trashed + else workspaces.c.trashed_at.is_(None) + ) + # Select total count from base_query subquery = base_query.subquery() count_query = select(func.count()).select_from(subquery) From c4310b9ee7d02523ae606649a88ebb9bf45edab5 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:19:23 +0100 Subject: [PATCH 12/52] migration --- ...6a50398c28a_trash_columns_in_workspaces.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/46a50398c28a_trash_columns_in_workspaces.py diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/46a50398c28a_trash_columns_in_workspaces.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/46a50398c28a_trash_columns_in_workspaces.py new file mode 100644 index 00000000000..1918a30d49c --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/46a50398c28a_trash_columns_in_workspaces.py @@ -0,0 +1,55 @@ +"""trash columns in workspaces + +Revision ID: 46a50398c28a +Revises: 8bfe65a5e294 +Create Date: 2024-11-12 17:18:46.668241+00:00 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "46a50398c28a" +down_revision = "8bfe65a5e294" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "workspaces", + sa.Column( + "trashed", + sa.DateTime(timezone=True), + nullable=True, + comment="The date and time when the workspace was marked as trashed.Null if the workspace has not been trashed [default].", + ), + ) + op.add_column( + "workspaces", + sa.Column( + "trashed_by", + sa.BigInteger(), + nullable=True, + comment="The user who marked the workspace as trashed or NULL if unknown or unmarked.", + ), + ) + op.create_foreign_key( + None, + "workspaces", + "users", + ["trashed_by"], + ["id"], + onupdate="CASCADE", + ondelete="SET NULL", + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "workspaces", type_="foreignkey") + op.drop_column("workspaces", "trashed_by") + op.drop_column("workspaces", "trashed") + # ### end Alembic commands ### From 44d56be34cf13e2949e8c3041b5e4966e2a39a76 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:26:49 +0100 Subject: [PATCH 13/52] common model --- api/specs/web-server/_trash.py | 13 ++++--------- packages/models-library/src/models_library/trash.py | 7 +++++++ .../simcore_service_webserver/folders/_models.py | 9 +++++---- .../simcore_service_webserver/workspaces/_models.py | 6 ++++++ .../workspaces/_workspaces_handlers.py | 5 +++++ 5 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 packages/models-library/src/models_library/trash.py diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index f66031a567d..dfb93cea7a5 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -8,15 +8,10 @@ from typing import Annotated from fastapi import APIRouter, Depends, status +from models_library.trash import RemoveQueryParams from simcore_service_webserver._meta import API_VTAG -from simcore_service_webserver.folders._models import ( - FoldersPathParams, - RemoveQueryParams, -) +from simcore_service_webserver.folders._models import FoldersPathParams from simcore_service_webserver.projects._trash_handlers import ProjectPathParams -from simcore_service_webserver.projects._trash_handlers import ( - RemoveQueryParams as RemoveQueryParams_duplicated, -) from simcore_service_webserver.workspaces._models import WorkspacesPathParams router = APIRouter( @@ -83,7 +78,7 @@ def untrash_project( ) def trash_folder( _path: Annotated[FoldersPathParams, Depends()], - _query: Annotated[RemoveQueryParams_duplicated, Depends()], + _query: Annotated[RemoveQueryParams, Depends()], ): ... @@ -116,7 +111,7 @@ def untrash_folder( ) def trash_workspace( _p: Annotated[WorkspacesPathParams, Depends()], - _q: Annotated[RemoveQueryParams_duplicated, Depends()], + _q: Annotated[RemoveQueryParams, Depends()], ): ... diff --git a/packages/models-library/src/models_library/trash.py b/packages/models-library/src/models_library/trash.py new file mode 100644 index 00000000000..306787ab60f --- /dev/null +++ b/packages/models-library/src/models_library/trash.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field + + +class RemoveQueryParams(BaseModel): + force: bool = Field( + default=False, description="Force removal (even if resource is active)" + ) diff --git a/services/web/server/src/simcore_service_webserver/folders/_models.py b/services/web/server/src/simcore_service_webserver/folders/_models.py index 766b34bf995..b1c920bfe50 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_models.py +++ b/services/web/server/src/simcore_service_webserver/folders/_models.py @@ -10,6 +10,7 @@ create_ordering_query_model_classes, ) from models_library.rest_pagination import PageQueryParameters +from models_library.trash import RemoveQueryParams from models_library.users import UserID from models_library.utils.common_validators import ( empty_str_to_none_pre_validator, @@ -93,7 +94,7 @@ class Config: extra = Extra.forbid -class RemoveQueryParams(BaseModel): - force: bool = Field( - default=False, description="Force removal (even if resource is active)" - ) + +assert RemoveQueryParams # nosec + +__all__: tuple[str, ...] = ("RemoveQueryParams",) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_models.py b/services/web/server/src/simcore_service_webserver/workspaces/_models.py index 3a366ad7cba..7d5c70eef21 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_models.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_models.py @@ -9,6 +9,7 @@ create_ordering_query_model_classes, ) from models_library.rest_pagination import PageQueryParameters +from models_library.trash import RemoveQueryParams from models_library.users import GroupID, UserID from models_library.workspaces import WorkspaceID from pydantic import BaseModel, Extra, Field @@ -70,3 +71,8 @@ class WorkspacesGroupsBodyParams(BaseModel): class Config: extra = Extra.forbid + + +assert RemoveQueryParams # nosec + +__all__: tuple[str, ...] = ("RemoveQueryParams",) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index 2e650647c4e..851ffdd7f89 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -30,6 +30,7 @@ WorkspacesListQueryParams, WorkspacesPathParams, WorkspacesRequestContext, + WorkspacesFilters ) _logger = logging.getLogger(__name__) @@ -68,10 +69,14 @@ async def list_workspaces(request: web.Request): WorkspacesListQueryParams, request ) + if not query_params.filters: + query_params.filters = WorkspacesFilters() + workspaces: WorkspaceGetPage = await _workspaces_api.list_workspaces( app=request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, + trashed=query_params.filters.trashed, offset=query_params.offset, limit=query_params.limit, order_by=parse_obj_as(OrderBy, query_params.order_by), From 8ee93da3a88a40be41a9f0a50ec17d6f2ab294eb Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:27:04 +0100 Subject: [PATCH 14/52] cleanup --- .../workspaces/_workspaces_api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py index a645037f5a4..4b13bb89440 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py @@ -23,6 +23,7 @@ async def create_workspace( app: web.Application, + *, user_id: UserID, name: str, description: str | None, @@ -59,6 +60,7 @@ async def create_workspace( async def get_workspace( app: web.Application, + *, user_id: UserID, workspace_id: WorkspaceID, product_name: ProductName, @@ -84,8 +86,10 @@ async def get_workspace( async def list_workspaces( app: web.Application, + *, user_id: UserID, product_name: ProductName, + trashed: bool, offset: NonNegativeInt, limit: int, order_by: OrderBy, @@ -94,6 +98,7 @@ async def list_workspaces( app, user_id=user_id, product_name=product_name, + trashed=trashed, offset=offset, limit=limit, order_by=order_by, @@ -119,6 +124,7 @@ async def list_workspaces( async def update_workspace( app: web.Application, + *, user_id: UserID, workspace_id: WorkspaceID, name: str, @@ -161,6 +167,7 @@ async def update_workspace( async def delete_workspace( app: web.Application, + *, user_id: UserID, workspace_id: WorkspaceID, product_name: ProductName, From e0a31135cbd32de0a6d9c7e895809aed0297a246 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:12:40 +0100 Subject: [PATCH 15/52] models --- .../server/src/simcore_service_webserver/folders/_models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/folders/_models.py b/services/web/server/src/simcore_service_webserver/folders/_models.py index b1c920bfe50..e6ca8b0a775 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_models.py +++ b/services/web/server/src/simcore_service_webserver/folders/_models.py @@ -17,7 +17,7 @@ null_or_none_str_to_none_validator, ) from models_library.workspaces import WorkspaceID -from pydantic import BaseModel, Extra, Field, validator +from pydantic import Extra, Field, validator from servicelib.request_keys import RQT_USERID_KEY from .._constants import RQ_PRODUCT_KEY @@ -94,7 +94,6 @@ class Config: extra = Extra.forbid - assert RemoveQueryParams # nosec __all__: tuple[str, ...] = ("RemoveQueryParams",) From bac1ac6be8daf080a5c5b7605f4bf0955495cf33 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:13:42 +0100 Subject: [PATCH 16/52] workspaces query --- api/specs/web-server/_workspaces.py | 26 +++++++------------ .../workspaces/_workspaces_handlers.py | 1 + 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/api/specs/web-server/_workspaces.py b/api/specs/web-server/_workspaces.py index 5e0180024f1..0e81bf7a78d 100644 --- a/api/specs/web-server/_workspaces.py +++ b/api/specs/web-server/_workspaces.py @@ -23,10 +23,18 @@ WorkspacesFilters, WorkspacesGroupsBodyParams, WorkspacesGroupsPathParams, - WorkspacesListWithJsonStrQueryParams, + WorkspacesOrderByJsonQueryParams, WorkspacesPathParams, ) + +class _ListQueryParams(WorkspacesOrderByJsonQueryParams): + filters: Annotated[ + Json | None, + Query(description=WorkspacesFilters.schema_json(indent=1)), + ] = None + + router = APIRouter( prefix=f"/{API_VTAG}", tags=[ @@ -51,21 +59,7 @@ async def create_workspace( response_model=Envelope[list[WorkspaceGet]], ) async def list_workspaces( - order_by: Annotated[ - Json, - Query( - description=WorkspacesListWithJsonStrQueryParams.__fields__[ - "order_by" - ].field_info.description, - example=WorkspacesListWithJsonStrQueryParams.__fields__[ - "order_by" - ].field_info.extra.get("example"), - ), - ] = WorkspacesListWithJsonStrQueryParams.__fields__["order_by"].field_info.default, - filters: Annotated[ - Json | None, - Query(description=WorkspacesFilters.schema_json(indent=1)), - ] = None, + _q: Annotated[_ListQueryParams, Depends()], ): ... diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index 851ffdd7f89..a57939913e0 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -72,6 +72,7 @@ async def list_workspaces(request: web.Request): if not query_params.filters: query_params.filters = WorkspacesFilters() + assert query_params.filters workspaces: WorkspaceGetPage = await _workspaces_api.list_workspaces( app=request.app, user_id=req_ctx.user_id, From 33cbd9d5d35f2340ad50f4875e116b9c2ad72b27 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:37:57 +0100 Subject: [PATCH 17/52] rest ordering --- .../service-library/tests/aiohttp/test_requests_validation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/service-library/tests/aiohttp/test_requests_validation.py b/packages/service-library/tests/aiohttp/test_requests_validation.py index 003f363f6e2..aa5643e3a07 100644 --- a/packages/service-library/tests/aiohttp/test_requests_validation.py +++ b/packages/service-library/tests/aiohttp/test_requests_validation.py @@ -20,6 +20,8 @@ from pydantic import BaseModel, Field from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( + RequestParams, + StrictRequestParams, parse_request_body_as, parse_request_headers_as, parse_request_path_parameters_as, From 6db1ea798f365d5690c0dcb9dfe63059329d15ee Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:55:15 +0100 Subject: [PATCH 18/52] base rest --- .../service-library/tests/aiohttp/test_requests_validation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/service-library/tests/aiohttp/test_requests_validation.py b/packages/service-library/tests/aiohttp/test_requests_validation.py index aa5643e3a07..003f363f6e2 100644 --- a/packages/service-library/tests/aiohttp/test_requests_validation.py +++ b/packages/service-library/tests/aiohttp/test_requests_validation.py @@ -20,8 +20,6 @@ from pydantic import BaseModel, Field from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( - RequestParams, - StrictRequestParams, parse_request_body_as, parse_request_headers_as, parse_request_path_parameters_as, From 21aa58d065d6f1a983c523e97886ef7473876734 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:02:42 +0100 Subject: [PATCH 19/52] cleanup api --- api/specs/web-server/_workspaces.py | 39 +++++++++++------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/api/specs/web-server/_workspaces.py b/api/specs/web-server/_workspaces.py index 0e81bf7a78d..22f159452f4 100644 --- a/api/specs/web-server/_workspaces.py +++ b/api/specs/web-server/_workspaces.py @@ -9,32 +9,23 @@ from enum import Enum from typing import Annotated -from fastapi import APIRouter, Depends, Query, status +from _common import as_query +from fastapi import APIRouter, Depends, status from models_library.api_schemas_webserver.workspaces import ( CreateWorkspaceBodyParams, PutWorkspaceBodyParams, WorkspaceGet, ) from models_library.generics import Envelope -from pydantic import Json from simcore_service_webserver._meta import API_VTAG from simcore_service_webserver.workspaces._groups_api import WorkspaceGroupGet from simcore_service_webserver.workspaces._models import ( - WorkspacesFilters, WorkspacesGroupsBodyParams, WorkspacesGroupsPathParams, - WorkspacesOrderByJsonQueryParams, + WorkspacesListQueryParams, WorkspacesPathParams, ) - -class _ListQueryParams(WorkspacesOrderByJsonQueryParams): - filters: Annotated[ - Json | None, - Query(description=WorkspacesFilters.schema_json(indent=1)), - ] = None - - router = APIRouter( prefix=f"/{API_VTAG}", tags=[ @@ -49,7 +40,7 @@ class _ListQueryParams(WorkspacesOrderByJsonQueryParams): status_code=status.HTTP_201_CREATED, ) async def create_workspace( - _b: CreateWorkspaceBodyParams, + _body: CreateWorkspaceBodyParams, ): ... @@ -59,7 +50,7 @@ async def create_workspace( response_model=Envelope[list[WorkspaceGet]], ) async def list_workspaces( - _q: Annotated[_ListQueryParams, Depends()], + _query: Annotated[as_query(WorkspacesListQueryParams), Depends()], ): ... @@ -69,7 +60,7 @@ async def list_workspaces( response_model=Envelope[WorkspaceGet], ) async def get_workspace( - _p: Annotated[WorkspacesPathParams, Depends()], + _path: Annotated[WorkspacesPathParams, Depends()], ): ... @@ -79,8 +70,8 @@ async def get_workspace( response_model=Envelope[WorkspaceGet], ) async def replace_workspace( - _p: Annotated[WorkspacesPathParams, Depends()], - _b: PutWorkspaceBodyParams, + _path: Annotated[WorkspacesPathParams, Depends()], + _body: PutWorkspaceBodyParams, ): ... @@ -90,7 +81,7 @@ async def replace_workspace( status_code=status.HTTP_204_NO_CONTENT, ) async def delete_workspace( - _p: Annotated[WorkspacesPathParams, Depends()], + _path: Annotated[WorkspacesPathParams, Depends()], ): ... @@ -106,8 +97,8 @@ async def delete_workspace( tags=_extra_tags, ) async def create_workspace_group( - _p: Annotated[WorkspacesGroupsPathParams, Depends()], - _b: WorkspacesGroupsBodyParams, + _path: Annotated[WorkspacesGroupsPathParams, Depends()], + _body: WorkspacesGroupsBodyParams, ): ... @@ -118,7 +109,7 @@ async def create_workspace_group( tags=_extra_tags, ) async def list_workspace_groups( - _p: Annotated[WorkspacesPathParams, Depends()], + _path: Annotated[WorkspacesPathParams, Depends()], ): ... @@ -129,8 +120,8 @@ async def list_workspace_groups( tags=_extra_tags, ) async def replace_workspace_group( - _p: Annotated[WorkspacesGroupsPathParams, Depends()], - _b: WorkspacesGroupsBodyParams, + _path: Annotated[WorkspacesGroupsPathParams, Depends()], + _body: WorkspacesGroupsBodyParams, ): ... @@ -141,6 +132,6 @@ async def replace_workspace_group( tags=_extra_tags, ) async def delete_workspace_group( - _p: Annotated[WorkspacesGroupsPathParams, Depends()], + _path: Annotated[WorkspacesGroupsPathParams, Depends()], ): ... From 716fe6f7e2d6388fee3bf49a92368d9a88179e42 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:03:14 +0100 Subject: [PATCH 20/52] openapi --- .../api/v0/openapi.yaml | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 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 f7c96e11a40..d1999a35ef4 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 @@ -2610,10 +2610,10 @@ paths: schema: title: Order By type: string - description: Order by field (`description|modified|name`) and direction - (`asc|desc`). The default sorting order is `{"field":"modified","direction":"desc"}`. + description: Order by field (`modified_at|name`) and direction (`asc|desc`). + The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"some_field_name","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -2693,10 +2693,10 @@ paths: schema: title: Order By type: string - description: Order by field (`description|modified|name`) and direction - (`asc|desc`). The default sorting order is `{"field":"modified","direction":"desc"}`. + description: Order by field (`modified_at|name`) and direction (`asc|desc`). + The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"some_field_name","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -3158,7 +3158,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"some_field_name","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -3381,7 +3381,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"some_field_name","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -4612,7 +4612,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"some_field_name","direction":"desc"}' + example: '{"field":"node_name","direction":"desc"}' name: order_by in: query - required: false @@ -4726,7 +4726,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"some_field_name","direction":"desc"}' + example: '{"field":"node_name","direction":"desc"}' name: order_by in: query - required: false @@ -5801,6 +5801,42 @@ paths: - workspaces summary: List Workspaces operationId: list_workspaces + parameters: + - required: false + schema: + title: Order By + type: string + description: Order by field (`modified_at|name`) and direction (`asc|desc`). + The default sorting order is `{"field":"modified_at","direction":"desc"}`. + default: '{"field":"modified","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' + name: order_by + in: query + - required: false + schema: + title: Filters + type: string + description: Custom filter query parameter encoded as JSON + name: filters + in: query + - required: false + schema: + title: Limit + exclusiveMaximum: true + minimum: 1 + type: integer + default: 20 + maximum: 50 + name: limit + in: query + - required: false + schema: + title: Offset + minimum: 0 + type: integer + default: 0 + name: offset + in: query responses: '200': description: Successful Response From 7e42e9f0dd0256f0a24272776a35dfbf9ce61503 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:04:18 +0100 Subject: [PATCH 21/52] updates oas --- api/specs/web-server/_trash.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index dfb93cea7a5..83a859de40b 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -110,8 +110,8 @@ def untrash_folder( }, ) def trash_workspace( - _p: Annotated[WorkspacesPathParams, Depends()], - _q: Annotated[RemoveQueryParams, Depends()], + _path: Annotated[WorkspacesPathParams, Depends()], + _query: Annotated[RemoveQueryParams, Depends()], ): ... @@ -122,6 +122,6 @@ def trash_workspace( status_code=status.HTTP_204_NO_CONTENT, ) def untrash_workspace( - _p: Annotated[WorkspacesPathParams, Depends()], + _path: Annotated[WorkspacesPathParams, Depends()], ): ... From be372fb55619a931222dcf0514efeed11a648cce Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:04:47 +0100 Subject: [PATCH 22/52] updates OAS --- .../simcore_service_webserver/api/v0/openapi.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 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 d1999a35ef4..03f350b8d09 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 @@ -2613,7 +2613,7 @@ paths: description: Order by field (`modified_at|name`) and direction (`asc|desc`). The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' + example: '{"field":"modified_at","direction":"desc"}' name: order_by in: query - required: false @@ -2696,7 +2696,7 @@ paths: description: Order by field (`modified_at|name`) and direction (`asc|desc`). The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' + example: '{"field":"modified_at","direction":"desc"}' name: order_by in: query - required: false @@ -3158,7 +3158,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' + example: '{"field":"prj_owner","direction":"desc"}' name: order_by in: query - required: false @@ -3381,7 +3381,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' + example: '{"field":"prj_owner","direction":"desc"}' name: order_by in: query - required: false @@ -4612,7 +4612,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"node_name","direction":"desc"}' + example: '{"field":"service_key","direction":"desc"}' name: order_by in: query - required: false @@ -4726,7 +4726,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"node_name","direction":"desc"}' + example: '{"field":"service_key","direction":"desc"}' name: order_by in: query - required: false @@ -5809,7 +5809,7 @@ paths: description: Order by field (`modified_at|name`) and direction (`asc|desc`). The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' + example: '{"field":"modified_at","direction":"desc"}' name: order_by in: query - required: false From 141c75936b245bd6a1b98d8f9d91d89fe41bcd87 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:05:42 +0100 Subject: [PATCH 23/52] updates migration --- .../src/models_library/utils/enums.py | 4 +++- ...=> aff5aa99ee3c_trash_columns_in_workspaces.py} | 14 +++++++------- .../simcore_postgres_database/models/_common.py | 5 ++--- 3 files changed, 12 insertions(+), 11 deletions(-) rename packages/postgres-database/src/simcore_postgres_database/migration/versions/{46a50398c28a_trash_columns_in_workspaces.py => aff5aa99ee3c_trash_columns_in_workspaces.py} (77%) diff --git a/packages/models-library/src/models_library/utils/enums.py b/packages/models-library/src/models_library/utils/enums.py index 7f0ff7eaf48..d0f3aa61f4e 100644 --- a/packages/models-library/src/models_library/utils/enums.py +++ b/packages/models-library/src/models_library/utils/enums.py @@ -6,7 +6,9 @@ @unique class StrAutoEnum(StrEnum): @staticmethod - def _generate_next_value_(name, start, count, last_values): + def _generate_next_value_( + name: str, start: str, count: int, last_values: int + ) -> str: return name.upper() diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/46a50398c28a_trash_columns_in_workspaces.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/aff5aa99ee3c_trash_columns_in_workspaces.py similarity index 77% rename from packages/postgres-database/src/simcore_postgres_database/migration/versions/46a50398c28a_trash_columns_in_workspaces.py rename to packages/postgres-database/src/simcore_postgres_database/migration/versions/aff5aa99ee3c_trash_columns_in_workspaces.py index 1918a30d49c..4916727de56 100644 --- a/packages/postgres-database/src/simcore_postgres_database/migration/versions/46a50398c28a_trash_columns_in_workspaces.py +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/aff5aa99ee3c_trash_columns_in_workspaces.py @@ -1,16 +1,16 @@ """trash columns in workspaces -Revision ID: 46a50398c28a -Revises: 8bfe65a5e294 -Create Date: 2024-11-12 17:18:46.668241+00:00 +Revision ID: aff5aa99ee3c +Revises: 8e1f83486be7 +Create Date: 2024-11-20 13:27:26.677261+00:00 """ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = "46a50398c28a" -down_revision = "8bfe65a5e294" +revision = "aff5aa99ee3c" +down_revision = "8e1f83486be7" branch_labels = None depends_on = None @@ -23,7 +23,7 @@ def upgrade(): "trashed", sa.DateTime(timezone=True), nullable=True, - comment="The date and time when the workspace was marked as trashed.Null if the workspace has not been trashed [default].", + comment="The date and time when the workspace was marked as trashed. Null if the workspace has not been trashed [default].", ), ) op.add_column( @@ -32,7 +32,7 @@ def upgrade(): "trashed_by", sa.BigInteger(), nullable=True, - comment="The user who marked the workspace as trashed or NULL if unknown or unmarked.", + comment="User who trashed the workspace, or null if not trashed or user is unknown.", ), ) op.create_foreign_key( diff --git a/packages/postgres-database/src/simcore_postgres_database/models/_common.py b/packages/postgres-database/src/simcore_postgres_database/models/_common.py index 8aaa1e08124..88cab05ed1b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/_common.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/_common.py @@ -63,7 +63,7 @@ def column_trashed_datetime(resource_name: str) -> sa.Column: "trashed", sa.DateTime(timezone=True), nullable=True, - comment=f"The date and time when the {resource_name} was marked as trashed." + comment=f"The date and time when the {resource_name} was marked as trashed. " f"Null if the {resource_name} has not been trashed [default].", ) @@ -78,8 +78,7 @@ def column_trashed_by_user(resource_name: str, users_table: sa.Table) -> sa.Colu ondelete="SET NULL", ), nullable=True, - comment=f"The user who marked the {resource_name} as trashed " - "or NULL if unknown or unmarked.", + comment=f"User who trashed the {resource_name}, or null if not trashed or user is unknown.", ) From 8e65a1a18aebf9bd67f475e473c683063e149115 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:36:59 +0100 Subject: [PATCH 24/52] folders --- api/specs/web-server/_trash.py | 7 +++++-- .../src/simcore_service_webserver/folders/_models.py | 5 ++--- .../simcore_service_webserver/folders/_trash_handlers.py | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index 83a859de40b..692894c31ab 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -10,7 +10,10 @@ from fastapi import APIRouter, Depends, status from models_library.trash import RemoveQueryParams from simcore_service_webserver._meta import API_VTAG -from simcore_service_webserver.folders._models import FoldersPathParams +from simcore_service_webserver.folders._models import ( + FoldersPathParams, + FolderTrashQueryParams, +) from simcore_service_webserver.projects._trash_handlers import ProjectPathParams from simcore_service_webserver.workspaces._models import WorkspacesPathParams @@ -78,7 +81,7 @@ def untrash_project( ) def trash_folder( _path: Annotated[FoldersPathParams, Depends()], - _query: Annotated[RemoveQueryParams, Depends()], + _query: Annotated[FolderTrashQueryParams, Depends()], ): ... diff --git a/services/web/server/src/simcore_service_webserver/folders/_models.py b/services/web/server/src/simcore_service_webserver/folders/_models.py index e6ca8b0a775..a61dab0a8fc 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_models.py +++ b/services/web/server/src/simcore_service_webserver/folders/_models.py @@ -94,6 +94,5 @@ class Config: extra = Extra.forbid -assert RemoveQueryParams # nosec - -__all__: tuple[str, ...] = ("RemoveQueryParams",) +class FolderTrashQueryParams(RemoveQueryParams): + ... diff --git a/services/web/server/src/simcore_service_webserver/folders/_trash_handlers.py b/services/web/server/src/simcore_service_webserver/folders/_trash_handlers.py index 55b53fcd4ee..396ee0490fa 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_trash_handlers.py +++ b/services/web/server/src/simcore_service_webserver/folders/_trash_handlers.py @@ -14,7 +14,7 @@ from ..security.decorators import permission_required from . import _trash_api from ._exceptions_handlers import handle_plugin_requests_exceptions -from ._models import FoldersPathParams, RemoveQueryParams +from ._models import FoldersPathParams, FolderTrashQueryParams _logger = logging.getLogger(__name__) @@ -31,8 +31,8 @@ async def trash_folder(request: web.Request): user_id = get_user_id(request) product_name = get_product_name(request) path_params = parse_request_path_parameters_as(FoldersPathParams, request) - query_params: RemoveQueryParams = parse_request_query_parameters_as( - RemoveQueryParams, request + query_params: FolderTrashQueryParams = parse_request_query_parameters_as( + FolderTrashQueryParams, request ) await _trash_api.trash_folder( From 6431182c5c29f44199621f59d1ad71dba59a077f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:42:14 +0100 Subject: [PATCH 25/52] workspaces query --- api/specs/web-server/_trash.py | 7 +++++-- .../src/simcore_service_webserver/workspaces/_models.py | 5 ++--- .../workspaces/_trash_handlers.py | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index 692894c31ab..9aad0748d5b 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -15,7 +15,10 @@ FolderTrashQueryParams, ) from simcore_service_webserver.projects._trash_handlers import ProjectPathParams -from simcore_service_webserver.workspaces._models import WorkspacesPathParams +from simcore_service_webserver.workspaces._models import ( + WorkspacesPathParams, + WorkspaceTrashQueryParams, +) router = APIRouter( prefix=f"/{API_VTAG}", @@ -114,7 +117,7 @@ def untrash_folder( ) def trash_workspace( _path: Annotated[WorkspacesPathParams, Depends()], - _query: Annotated[RemoveQueryParams, Depends()], + _query: Annotated[WorkspaceTrashQueryParams, Depends()], ): ... diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_models.py b/services/web/server/src/simcore_service_webserver/workspaces/_models.py index 7d5c70eef21..55b5100d050 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_models.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_models.py @@ -73,6 +73,5 @@ class Config: extra = Extra.forbid -assert RemoveQueryParams # nosec - -__all__: tuple[str, ...] = ("RemoveQueryParams",) +class WorkspaceTrashQueryParams(RemoveQueryParams): + ... diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py index dcd45ea8d89..d245cf0ac42 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py @@ -14,7 +14,7 @@ from ..security.decorators import permission_required from . import _trash_api from ._exceptions_handlers import handle_plugin_requests_exceptions -from ._models import RemoveQueryParams, WorkspacesPathParams +from ._models import WorkspacesPathParams, WorkspaceTrashQueryParams _logger = logging.getLogger(__name__) @@ -31,8 +31,8 @@ async def trash_workspace(request: web.Request): user_id = get_user_id(request) product_name = get_product_name(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) - query_params: RemoveQueryParams = parse_request_query_parameters_as( - RemoveQueryParams, request + query_params: WorkspaceTrashQueryParams = parse_request_query_parameters_as( + WorkspaceTrashQueryParams, request ) await _trash_api.trash_workspace( From 20701350ab02fa7a6f30f4f760666eaebaa9745a Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:43:56 +0100 Subject: [PATCH 26/52] workspaces query --- .../src/simcore_service_webserver/workspaces/_workspaces_db.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index 7c1366e993e..f4d170daef3 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -50,6 +50,8 @@ workspaces.c.trashed_by, ) +assert set(WorkspaceDB.__fields__) == {c.name for c in _SELECTION_ARGS} # nosec + async def create_workspace( app: web.Application, From 1cca7577c264338b7153deaec5de3ef852226cbc Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:55:58 +0100 Subject: [PATCH 27/52] clenup --- api/specs/web-server/_trash.py | 4 ++-- api/specs/web-server/_workspaces.py | 8 ++++---- .../models_library/api_schemas_webserver/workspaces.py | 4 ++-- .../simcore_service_webserver/workspaces/_models.py | 4 ++-- .../workspaces/_workspaces_handlers.py | 10 +++++----- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index 9aad0748d5b..06411ed38e5 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -77,7 +77,7 @@ def untrash_project( responses={ status.HTTP_404_NOT_FOUND: {"description": "Not such a folder"}, status.HTTP_409_CONFLICT: { - "description": "One or more projects are in use and cannot be trashed" + "description": "One or more projects in the folder are in use and cannot be trashed" }, status.HTTP_503_SERVICE_UNAVAILABLE: {"description": "Trash service error"}, }, @@ -110,7 +110,7 @@ def untrash_folder( responses={ status.HTTP_404_NOT_FOUND: {"description": "Not such a workspace"}, status.HTTP_409_CONFLICT: { - "description": "One or more projects are in use and cannot be trashed" + "description": "One or more projects in the workspace are in use and cannot be trashed" }, status.HTTP_503_SERVICE_UNAVAILABLE: {"description": "Trash service error"}, }, diff --git a/api/specs/web-server/_workspaces.py b/api/specs/web-server/_workspaces.py index 22f159452f4..958b8457bca 100644 --- a/api/specs/web-server/_workspaces.py +++ b/api/specs/web-server/_workspaces.py @@ -12,9 +12,9 @@ from _common import as_query from fastapi import APIRouter, Depends, status from models_library.api_schemas_webserver.workspaces import ( - CreateWorkspaceBodyParams, - PutWorkspaceBodyParams, + WorkspaceCreateBodyParams, WorkspaceGet, + WorkspaceReplaceBodyParams, ) from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG @@ -40,7 +40,7 @@ status_code=status.HTTP_201_CREATED, ) async def create_workspace( - _body: CreateWorkspaceBodyParams, + _body: WorkspaceCreateBodyParams, ): ... @@ -71,7 +71,7 @@ async def get_workspace( ) async def replace_workspace( _path: Annotated[WorkspacesPathParams, Depends()], - _body: PutWorkspaceBodyParams, + _body: WorkspaceReplaceBodyParams, ): ... diff --git a/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py b/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py index 0ba98ab4ec3..414869739d8 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py @@ -26,7 +26,7 @@ class WorkspaceGetPage(NamedTuple): total: PositiveInt -class CreateWorkspaceBodyParams(InputSchema): +class WorkspaceCreateBodyParams(InputSchema): name: str description: str | None = None thumbnail: str | None = None @@ -35,7 +35,7 @@ class Config: extra = Extra.forbid -class PutWorkspaceBodyParams(InputSchema): +class WorkspaceReplaceBodyParams(InputSchema): name: IDStr description: str | None = None thumbnail: str | None = None diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_models.py b/services/web/server/src/simcore_service_webserver/workspaces/_models.py index 55b5100d050..6c080c2f988 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_models.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_models.py @@ -29,7 +29,7 @@ class WorkspacesPathParams(StrictRequestParameters): workspace_id: WorkspaceID -WorkspacesListOrderQueryParams: type[ +_WorkspacesListOrderQueryParams: type[ RequestParameters ] = create_ordering_query_model_classes( ordering_fields={ @@ -51,7 +51,7 @@ class WorkspacesFilters(Filters): class WorkspacesListQueryParams( PageQueryParameters, FiltersQueryParameters[WorkspacesFilters], - WorkspacesListOrderQueryParams, # type: ignore[misc, valid-type] + _WorkspacesListOrderQueryParams, # type: ignore[misc, valid-type] ): ... diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index a57939913e0..49b7f789727 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -2,10 +2,10 @@ from aiohttp import web from models_library.api_schemas_webserver.workspaces import ( - CreateWorkspaceBodyParams, - PutWorkspaceBodyParams, + WorkspaceCreateBodyParams, WorkspaceGet, WorkspaceGetPage, + WorkspaceReplaceBodyParams, ) from models_library.rest_ordering import OrderBy from models_library.rest_pagination import Page @@ -27,10 +27,10 @@ from . import _workspaces_api from ._exceptions_handlers import handle_plugin_requests_exceptions from ._models import ( + WorkspacesFilters, WorkspacesListQueryParams, WorkspacesPathParams, WorkspacesRequestContext, - WorkspacesFilters ) _logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ @handle_plugin_requests_exceptions async def create_workspace(request: web.Request): req_ctx = WorkspacesRequestContext.parse_obj(request) - body_params = await parse_request_body_as(CreateWorkspaceBodyParams, request) + body_params = await parse_request_body_as(WorkspaceCreateBodyParams, request) workspace: WorkspaceGet = await _workspaces_api.create_workspace( request.app, @@ -126,7 +126,7 @@ async def get_workspace(request: web.Request): async def replace_workspace(request: web.Request): req_ctx = WorkspacesRequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(WorkspacesPathParams, request) - body_params = await parse_request_body_as(PutWorkspaceBodyParams, request) + body_params = await parse_request_body_as(WorkspaceReplaceBodyParams, request) workspace: WorkspaceGet = await _workspaces_api.update_workspace( app=request.app, From 4484c54651f868a35a85d75e00ebda311f0f8d91 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:13:41 +0100 Subject: [PATCH 28/52] implements WorkspaceGet w/ trash attributes --- api/specs/web-server/_folders.py | 8 +-- .../api_schemas_webserver/folders_v2.py | 4 +- .../api_schemas_webserver/workspaces.py | 3 + .../folders/_folders_handlers.py | 8 +-- .../workspaces/_workspaces_api.py | 66 ++++++------------- .../workspaces/_workspaces_db.py | 6 +- .../workspaces/_workspaces_handlers.py | 2 +- 7 files changed, 38 insertions(+), 59 deletions(-) diff --git a/api/specs/web-server/_folders.py b/api/specs/web-server/_folders.py index c2e75579b26..f98b5e98308 100644 --- a/api/specs/web-server/_folders.py +++ b/api/specs/web-server/_folders.py @@ -12,9 +12,9 @@ from _common import as_query from fastapi import APIRouter, Depends, status from models_library.api_schemas_webserver.folders_v2 import ( - CreateFolderBodyParams, + FolderCreateBodyParams, FolderGet, - PutFolderBodyParams, + FolderReplaceBodyParams, ) from models_library.generics import Envelope from simcore_service_webserver._meta import API_VTAG @@ -38,7 +38,7 @@ status_code=status.HTTP_201_CREATED, ) async def create_folder( - _body: CreateFolderBodyParams, + _body: FolderCreateBodyParams, ): ... @@ -79,7 +79,7 @@ async def get_folder( ) async def replace_folder( _path: Annotated[FoldersPathParams, Depends()], - _body: PutFolderBodyParams, + _body: FolderReplaceBodyParams, ): ... diff --git a/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py b/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py index 4ba77e0e7c3..97a9111c494 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py @@ -29,7 +29,7 @@ class FolderGetPage(NamedTuple): total: PositiveInt -class CreateFolderBodyParams(InputSchema): +class FolderCreateBodyParams(InputSchema): name: IDStr parent_folder_id: FolderID | None = None workspace_id: WorkspaceID | None = None @@ -46,7 +46,7 @@ class Config: )(null_or_none_str_to_none_validator) -class PutFolderBodyParams(InputSchema): +class FolderReplaceBodyParams(InputSchema): name: IDStr parent_folder_id: FolderID | None diff --git a/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py b/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py index 414869739d8..1b318870f8f 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/workspaces.py @@ -7,6 +7,7 @@ from pydantic import Extra, PositiveInt from ..access_rights import AccessRights +from ..users import UserID from ._base import InputSchema, OutputSchema @@ -17,6 +18,8 @@ class WorkspaceGet(OutputSchema): thumbnail: str | None created_at: datetime modified_at: datetime + trashed_at: datetime | None + trashed_by: UserID | None my_access_rights: AccessRights access_rights: dict[GroupID, AccessRights] diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py b/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py index e8a888cf541..9b1b1a193bb 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py @@ -2,10 +2,10 @@ from aiohttp import web from models_library.api_schemas_webserver.folders_v2 import ( - CreateFolderBodyParams, + FolderCreateBodyParams, FolderGet, FolderGetPage, - PutFolderBodyParams, + FolderReplaceBodyParams, ) from models_library.rest_ordering import OrderBy from models_library.rest_pagination import Page @@ -46,7 +46,7 @@ @handle_plugin_requests_exceptions async def create_folder(request: web.Request): req_ctx = FoldersRequestContext.parse_obj(request) - body_params = await parse_request_body_as(CreateFolderBodyParams, request) + body_params = await parse_request_body_as(FolderCreateBodyParams, request) folder = await _folders_api.create_folder( request.app, @@ -167,7 +167,7 @@ async def get_folder(request: web.Request): async def replace_folder(request: web.Request): req_ctx = FoldersRequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(FoldersPathParams, request) - body_params = await parse_request_body_as(PutFolderBodyParams, request) + body_params = await parse_request_body_as(FolderReplaceBodyParams, request) folder = await _folders_api.update_folder( app=request.app, diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py index 4b13bb89440..944c2bcfa8d 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py @@ -21,6 +21,21 @@ _logger = logging.getLogger(__name__) +def _to_api_model(workspace_db: UserWorkspaceAccessRightsDB): + return WorkspaceGet( + workspace_id=workspace_db.workspace_id, + name=workspace_db.name, + description=workspace_db.description, + thumbnail=workspace_db.thumbnail, + created_at=workspace_db.created, + modified_at=workspace_db.modified, + trashed_at=workspace_db.trashed, + trashed_by=workspace_db.trashed_by if workspace_db.trashed else None, + my_access_rights=workspace_db.my_access_rights, + access_rights=workspace_db.access_rights, + ) + + async def create_workspace( app: web.Application, *, @@ -46,16 +61,7 @@ async def create_workspace( workspace_id=created_workspace_db.workspace_id, product_name=product_name, ) - return WorkspaceGet( - workspace_id=workspace_db.workspace_id, - name=workspace_db.name, - description=workspace_db.description, - thumbnail=workspace_db.thumbnail, - created_at=workspace_db.created, - modified_at=workspace_db.modified, - my_access_rights=workspace_db.my_access_rights, - access_rights=workspace_db.access_rights, - ) + return _to_api_model(workspace_db) async def get_workspace( @@ -72,16 +78,7 @@ async def get_workspace( product_name=product_name, permission="read", ) - return WorkspaceGet( - workspace_id=workspace_db.workspace_id, - name=workspace_db.name, - description=workspace_db.description, - thumbnail=workspace_db.thumbnail, - created_at=workspace_db.created, - modified_at=workspace_db.modified, - my_access_rights=workspace_db.my_access_rights, - access_rights=workspace_db.access_rights, - ) + return _to_api_model(workspace_db) async def list_workspaces( @@ -89,7 +86,7 @@ async def list_workspaces( *, user_id: UserID, product_name: ProductName, - trashed: bool, + filter_trashed: bool | None, offset: NonNegativeInt, limit: int, order_by: OrderBy, @@ -98,26 +95,14 @@ async def list_workspaces( app, user_id=user_id, product_name=product_name, - trashed=trashed, + filter_trashed=filter_trashed, offset=offset, limit=limit, order_by=order_by, ) return WorkspaceGetPage( - items=[ - WorkspaceGet( - workspace_id=workspace.workspace_id, - name=workspace.name, - description=workspace.description, - thumbnail=workspace.thumbnail, - created_at=workspace.created, - modified_at=workspace.modified, - my_access_rights=workspace.my_access_rights, - access_rights=workspace.access_rights, - ) - for workspace in workspaces - ], + items=[_to_api_model(workspace_db) for workspace_db in workspaces], total=total_count, ) @@ -153,16 +138,7 @@ async def update_workspace( workspace_id=workspace_id, product_name=product_name, ) - return WorkspaceGet( - workspace_id=workspace_db.workspace_id, - name=workspace_db.name, - description=workspace_db.description, - thumbnail=workspace_db.thumbnail, - created_at=workspace_db.created, - modified_at=workspace_db.modified, - my_access_rights=workspace_db.my_access_rights, - access_rights=workspace_db.access_rights, - ) + return _to_api_model(workspace_db) async def delete_workspace( diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index f4d170daef3..6c7714829be 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -109,7 +109,7 @@ async def list_workspaces_for_user( *, user_id: UserID, product_name: ProductName, - trashed: bool | None, + filter_trashed: bool | None, offset: NonNegativeInt, limit: NonNegativeInt, order_by: OrderBy, @@ -130,10 +130,10 @@ async def list_workspaces_for_user( .where(workspaces.c.product_name == product_name) ) - if trashed is not None: + if filter_trashed is not None: base_query = base_query.where( workspaces.c.trashed_at.is_not(None) - if trashed + if filter_trashed else workspaces.c.trashed_at.is_(None) ) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index 49b7f789727..390d7ab87df 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -77,7 +77,7 @@ async def list_workspaces(request: web.Request): app=request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, - trashed=query_params.filters.trashed, + filter_trashed=query_params.filters.trashed, offset=query_params.offset, limit=query_params.limit, order_by=parse_obj_as(OrderBy, query_params.order_by), From cca0c1a6958104ee6becdfbc37e158fea456889d Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:19:58 +0100 Subject: [PATCH 29/52] oas --- services/web/server/README.md | 1 + .../api/v0/openapi.yaml | 183 ++++++++++-------- 2 files changed, 98 insertions(+), 86 deletions(-) diff --git a/services/web/server/README.md b/services/web/server/README.md index 96e1b6dfa2f..c41841a85d2 100644 --- a/services/web/server/README.md +++ b/services/web/server/README.md @@ -1,5 +1,6 @@ # web/server +[![ReDoc](https://img.shields.io/badge/OpenAPI-ReDoc-85ea2d?logo=openapiinitiative)](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/ITISFoundation/osparc-simcore/refs/heads/master/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml) [![Docker Pulls](https://img.shields.io/docker/pulls/itisfoundation/webserver.svg)](https://hub.docker.com/r/itisfoundation/webserver/tags) [![](https://images.microbadger.com/badges/image/itisfoundation/webserver.svg)](https://microbadger.com/images/itisfoundation/webserver "More on service image in registry") [![](https://images.microbadger.com/badges/version/itisfoundation/webserver.svg)](https://microbadger.com/images/itisfoundation/webserver "More on service image in registry") 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 03f350b8d09..c6ec3af803b 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 @@ -2613,7 +2613,7 @@ paths: description: Order by field (`modified_at|name`) and direction (`asc|desc`). The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"modified_at","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -2666,7 +2666,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateFolderBodyParams' + $ref: '#/components/schemas/FolderCreateBodyParams' required: true responses: '201': @@ -2696,7 +2696,7 @@ paths: description: Order by field (`modified_at|name`) and direction (`asc|desc`). The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"modified_at","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -2771,7 +2771,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PutFolderBodyParams' + $ref: '#/components/schemas/FolderReplaceBodyParams' required: true responses: '200': @@ -3158,7 +3158,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"prj_owner","direction":"desc"}' + example: '{"field":"type","direction":"desc"}' name: order_by in: query - required: false @@ -3381,7 +3381,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"prj_owner","direction":"desc"}' + example: '{"field":"type","direction":"desc"}' name: order_by in: query - required: false @@ -4612,7 +4612,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"service_key","direction":"desc"}' + example: '{"field":"root_parent_project_id","direction":"desc"}' name: order_by in: query - required: false @@ -4726,7 +4726,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"service_key","direction":"desc"}' + example: '{"field":"root_parent_project_id","direction":"desc"}' name: order_by in: query - required: false @@ -5500,7 +5500,8 @@ paths: '404': description: Not such a folder '409': - description: One or more projects are in use and cannot be trashed + description: One or more projects in the folder are in use and cannot be + trashed '503': description: Trash service error /v0/folders/{folder_id}:untrash: @@ -5551,7 +5552,8 @@ paths: '404': description: Not such a workspace '409': - description: One or more projects are in use and cannot be trashed + description: One or more projects in the workspace are in use and cannot + be trashed '503': description: Trash service error /v0/workspace/{workspace_id}:untrash: @@ -5809,7 +5811,7 @@ paths: description: Order by field (`modified_at|name`) and direction (`asc|desc`). The default sorting order is `{"field":"modified_at","direction":"desc"}`. default: '{"field":"modified","direction":"desc"}' - example: '{"field":"modified_at","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -5853,7 +5855,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateWorkspaceBodyParams' + $ref: '#/components/schemas/WorkspaceCreateBodyParams' required: true responses: '201': @@ -5902,7 +5904,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PutWorkspaceBodyParams' + $ref: '#/components/schemas/WorkspaceReplaceBodyParams' required: true responses: '200': @@ -7070,28 +7072,6 @@ components: alpha2: title: Alpha2 type: string - CreateFolderBodyParams: - title: CreateFolderBodyParams - required: - - name - type: object - properties: - name: - title: Name - maxLength: 100 - minLength: 1 - type: string - parentFolderId: - title: Parentfolderid - exclusiveMinimum: true - type: integer - minimum: 0 - workspaceId: - title: Workspaceid - exclusiveMinimum: true - type: integer - minimum: 0 - additionalProperties: false CreatePricingPlanBodyParams: title: CreatePricingPlanBodyParams required: @@ -7171,22 +7151,6 @@ components: title: Comment maxLength: 100 type: string - CreateWorkspaceBodyParams: - title: CreateWorkspaceBodyParams - required: - - name - type: object - properties: - name: - title: Name - type: string - description: - title: Description - type: string - thumbnail: - title: Thumbnail - type: string - additionalProperties: false DatCoreFileLink: title: DatCoreFileLink required: @@ -8594,6 +8558,28 @@ components: format: uri links: $ref: '#/components/schemas/FileUploadLinks' + FolderCreateBodyParams: + title: FolderCreateBodyParams + required: + - name + type: object + properties: + name: + title: Name + maxLength: 100 + minLength: 1 + type: string + parentFolderId: + title: Parentfolderid + exclusiveMinimum: true + type: integer + minimum: 0 + workspaceId: + title: Workspaceid + exclusiveMinimum: true + type: integer + minimum: 0 + additionalProperties: false FolderGet: title: FolderGet required: @@ -8642,6 +8628,23 @@ components: minimum: 0 myAccessRights: $ref: '#/components/schemas/models_library__access_rights__AccessRights' + FolderReplaceBodyParams: + title: FolderReplaceBodyParams + required: + - name + type: object + properties: + name: + title: Name + maxLength: 100 + minLength: 1 + type: string + parentFolderId: + title: Parentfolderid + exclusiveMinimum: true + type: integer + minimum: 0 + additionalProperties: false GenerateInvitation: title: GenerateInvitation required: @@ -11150,23 +11153,6 @@ components: description: Timestamp with last update format: date-time additionalProperties: false - PutFolderBodyParams: - title: PutFolderBodyParams - required: - - name - type: object - properties: - name: - title: Name - maxLength: 100 - minLength: 1 - type: string - parentFolderId: - title: Parentfolderid - exclusiveMinimum: true - type: integer - minimum: 0 - additionalProperties: false PutWalletBodyParams: title: PutWalletBodyParams required: @@ -11185,24 +11171,6 @@ components: type: string status: $ref: '#/components/schemas/WalletStatus' - PutWorkspaceBodyParams: - title: PutWorkspaceBodyParams - required: - - name - type: object - properties: - name: - title: Name - maxLength: 100 - minLength: 1 - type: string - description: - title: Description - type: string - thumbnail: - title: Thumbnail - type: string - additionalProperties: false RegisterBody: title: RegisterBody required: @@ -13182,6 +13150,22 @@ components: allOf: - $ref: '#/components/schemas/TaskCounts' description: task details + WorkspaceCreateBodyParams: + title: WorkspaceCreateBodyParams + required: + - name + type: object + properties: + name: + title: Name + type: string + description: + title: Description + type: string + thumbnail: + title: Thumbnail + type: string + additionalProperties: false WorkspaceGet: title: WorkspaceGet required: @@ -13215,6 +13199,15 @@ components: title: Modifiedat type: string format: date-time + trashedAt: + title: Trashedat + type: string + format: date-time + trashedBy: + title: Trashedby + exclusiveMinimum: true + type: integer + minimum: 0 myAccessRights: $ref: '#/components/schemas/models_library__access_rights__AccessRights' accessRights: @@ -13255,6 +13248,24 @@ components: title: Modified type: string format: date-time + WorkspaceReplaceBodyParams: + title: WorkspaceReplaceBodyParams + required: + - name + type: object + properties: + name: + title: Name + maxLength: 100 + minLength: 1 + type: string + description: + title: Description + type: string + thumbnail: + title: Thumbnail + type: string + additionalProperties: false WorkspacesGroupsBodyParams: title: WorkspacesGroupsBodyParams required: From 34531271c0330ce04d9c73e703c8b4bb0ff34643 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:33:28 +0100 Subject: [PATCH 30/52] cleanup --- .../with_dbs/04/workspaces/test_workspaces.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py index c45d2b43783..7de666c2511 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py @@ -50,7 +50,7 @@ async def test_workspaces_user_role_permissions( assert client.app url = client.app.router["list_workspaces"].url_for() - resp = await client.get(url.path) + resp = await client.get(f"{url}") await assert_status(resp, expected.ok) @@ -65,78 +65,75 @@ async def test_workspaces_workflow( # list user workspaces url = client.app.router["list_workspaces"].url_for() - resp = await client.get(url.path) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data == [] # create a new workspace url = client.app.router["create_workspace"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "My first workspace", "description": "Custom description", "thumbnail": None, }, ) - added_workspace, _ = await assert_status(resp, status.HTTP_201_CREATED) - assert WorkspaceGet.parse_obj(added_workspace) + data, _ = await assert_status(resp, status.HTTP_201_CREATED) + added_workspace = WorkspaceGet.parse_obj(data) # list user workspaces url = client.app.router["list_workspaces"].url_for() - resp = await client.get(url.path) + resp = await client.get(f"{url}") data, _, meta, links = await assert_status( resp, status.HTTP_200_OK, include_meta=True, include_links=True ) assert len(data) == 1 - assert data[0]["workspaceId"] == added_workspace["workspaceId"] - assert data[0]["name"] == "My first workspace" - assert data[0]["description"] == "Custom description" + assert data[0] == added_workspace.dict() assert meta["count"] == 1 assert links # get a user workspace url = client.app.router["get_workspace"].url_for( - workspace_id=f"{added_workspace['workspaceId']}" + workspace_id=f"{added_workspace.workspace_id}" ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) - assert data["workspaceId"] == added_workspace["workspaceId"] + assert data["workspaceId"] == added_workspace.workspace_id assert data["name"] == "My first workspace" assert data["description"] == "Custom description" # update a workspace url = client.app.router["replace_workspace"].url_for( - workspace_id=f"{added_workspace['workspaceId']}" + workspace_id=f"{added_workspace.workspace_id}" ) resp = await client.put( - url.path, + f"{url}", json={ "name": "My Second workspace", "description": "", }, ) data, _ = await assert_status(resp, status.HTTP_200_OK) - assert WorkspaceGet.parse_obj(data) + second_workspace = WorkspaceGet.parse_obj(data) # list user workspaces url = client.app.router["list_workspaces"].url_for() - resp = await client.get(url.path) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 - assert data[0]["name"] == "My Second workspace" - assert data[0]["description"] == "" + assert data[0] == second_workspace.dict() # delete a workspace url = client.app.router["delete_workspace"].url_for( - workspace_id=f"{added_workspace['workspaceId']}" + workspace_id=f"{added_workspace.workspace_id}" ) - resp = await client.delete(url.path) + resp = await client.delete(f"{url}") data, _ = await assert_status(resp, status.HTTP_204_NO_CONTENT) # list user workspaces url = client.app.router["list_workspaces"].url_for() - resp = await client.get(url.path) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data == [] @@ -151,3 +148,4 @@ async def test_project_workspace_movement_full_workflow( assert client.app # NOTE: MD: not yet implemented + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/6778 From f040534f214b792a6adc92c58b82100603f5f9a7 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:35:18 +0100 Subject: [PATCH 31/52] cleanup test --- .../with_dbs/04/workspaces/test_workspaces.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py index 7de666c2511..7306dfd341b 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py @@ -69,7 +69,7 @@ async def test_workspaces_workflow( data, _ = await assert_status(resp, status.HTTP_200_OK) assert data == [] - # create a new workspace + # CREATE a new workspace url = client.app.router["create_workspace"].url_for() resp = await client.post( f"{url}", @@ -82,7 +82,7 @@ async def test_workspaces_workflow( data, _ = await assert_status(resp, status.HTTP_201_CREATED) added_workspace = WorkspaceGet.parse_obj(data) - # list user workspaces + # LIST user workspaces url = client.app.router["list_workspaces"].url_for() resp = await client.get(f"{url}") data, _, meta, links = await assert_status( @@ -93,7 +93,7 @@ async def test_workspaces_workflow( assert meta["count"] == 1 assert links - # get a user workspace + # GET a user workspace url = client.app.router["get_workspace"].url_for( workspace_id=f"{added_workspace.workspace_id}" ) @@ -103,7 +103,7 @@ async def test_workspaces_workflow( assert data["name"] == "My first workspace" assert data["description"] == "Custom description" - # update a workspace + # REPLACE a workspace url = client.app.router["replace_workspace"].url_for( workspace_id=f"{added_workspace.workspace_id}" ) @@ -115,23 +115,23 @@ async def test_workspaces_workflow( }, ) data, _ = await assert_status(resp, status.HTTP_200_OK) - second_workspace = WorkspaceGet.parse_obj(data) + replaced_workspace = WorkspaceGet.parse_obj(data) - # list user workspaces + # LIST user workspaces url = client.app.router["list_workspaces"].url_for() resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 - assert data[0] == second_workspace.dict() + assert data[0] == replaced_workspace.dict() - # delete a workspace + # DELETE a workspace url = client.app.router["delete_workspace"].url_for( workspace_id=f"{added_workspace.workspace_id}" ) resp = await client.delete(f"{url}") data, _ = await assert_status(resp, status.HTTP_204_NO_CONTENT) - # list user workspaces + # LIST user workspaces url = client.app.router["list_workspaces"].url_for() resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) From 998966c2fe461f17a0b7ad9889ff0e6b9f2e632e Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:47:11 +0100 Subject: [PATCH 32/52] cleanup tests --- ...t_workspaces__folders_and_projects_crud.py | 186 ++++++++++-------- 1 file changed, 108 insertions(+), 78 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py index 717de9303fd..c6dedca6099 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces__folders_and_projects_crud.py @@ -12,6 +12,7 @@ import pytest from aiohttp.test_utils import TestClient +from models_library.api_schemas_webserver.workspaces import WorkspaceGet from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import LoggedUser, UserInfoDict @@ -54,21 +55,22 @@ async def test_workspaces_full_workflow_with_folders_and_projects( ): assert client.app - # create a new workspace + # Create a new workspace url = client.app.router["create_workspace"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "My first workspace", "description": "Custom description", "thumbnail": None, }, ) - added_workspace, _ = await assert_status(resp, status.HTTP_201_CREATED) + data, _ = await assert_status(resp, status.HTTP_201_CREATED) + added_workspace = WorkspaceGet.parse_obj(data) - # Create project in workspace + # Create project **in workspace** project_data = deepcopy(fake_project) - project_data["workspace_id"] = f"{added_workspace['workspaceId']}" + project_data["workspace_id"] = f"{added_workspace.workspace_id}" project = await create_project( client.app, project_data, @@ -77,30 +79,33 @@ async def test_workspaces_full_workflow_with_folders_and_projects( ) # List project in workspace - base_url = client.app.router["list_projects"].url_for() - url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + url = ( + client.app.router["list_projects"] + .url_for() + .with_query({"workspace_id": f"{added_workspace.workspace_id}"}) + ) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] - assert data[0]["workspaceId"] == added_workspace["workspaceId"] + assert data[0]["workspaceId"] == added_workspace.workspace_id assert data[0]["folderId"] is None # Get project in workspace - base_url = client.app.router["get_project"].url_for(project_id=project["uuid"]) - resp = await client.get(base_url) + url = client.app.router["get_project"].url_for(project_id=project["uuid"]) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert data["uuid"] == project["uuid"] - assert data["workspaceId"] == added_workspace["workspaceId"] + assert data["workspaceId"] == added_workspace.workspace_id assert data["folderId"] is None # Create folder in workspace url = client.app.router["create_folder"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "Original user folder", - "workspaceId": f"{added_workspace['workspaceId']}", + "workspaceId": f"{added_workspace.workspace_id}", }, ) first_folder, _ = await assert_status(resp, status.HTTP_201_CREATED) @@ -108,9 +113,9 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # List folders in workspace base_url = client.app.router["list_folders"].url_for() url = base_url.with_query( - {"workspace_id": f"{added_workspace['workspaceId']}", "folder_id": "null"} + {"workspace_id": f"{added_workspace.workspace_id}", "folder_id": "null"} ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["folderId"] == first_folder["folderId"] @@ -120,18 +125,18 @@ async def test_workspaces_full_workflow_with_folders_and_projects( folder_id=f"{first_folder['folderId']}", project_id=f"{project['uuid']}", ) - resp = await client.put(url.path) + resp = await client.put(f"{url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) # List projects in specific folder in workspace base_url = client.app.router["list_projects"].url_for() url = base_url.with_query( { - "workspace_id": f"{added_workspace['workspaceId']}", + "workspace_id": f"{added_workspace.workspace_id}", "folder_id": f"{first_folder['folderId']}", } ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] @@ -140,9 +145,12 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # Create new user async with LoggedUser(client) as new_logged_user: # Try to list folder that user doesn't have access to - base_url = client.app.router["list_projects"].url_for() - url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + url = ( + client.app.router["list_projects"] + .url_for() + .with_query({"workspace_id": f"{added_workspace.workspace_id}"}) + ) + resp = await client.get(f"{url}") _, errors = await assert_status( resp, status.HTTP_403_FORBIDDEN, @@ -152,7 +160,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # Now we will share the workspace with the new user await update_or_insert_workspace_group( client.app, - workspace_id=added_workspace["workspaceId"], + workspace_id=added_workspace.workspace_id, group_id=new_logged_user["primary_gid"], read=True, write=True, @@ -160,35 +168,44 @@ async def test_workspaces_full_workflow_with_folders_and_projects( ) # New user list root folders inside of workspace - base_url = client.app.router["list_folders"].url_for() - url = base_url.with_query( - {"workspace_id": f"{added_workspace['workspaceId']}", "folder_id": "null"} + url = ( + client.app.router["list_folders"] + .url_for() + .with_query( + {"workspace_id": f"{added_workspace.workspace_id}", "folder_id": "null"} + ) ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 # New user list root projects inside of workspace - base_url = client.app.router["list_projects"].url_for() - url = base_url.with_query( - { - "workspace_id": f"{added_workspace['workspaceId']}", - "folder_id": "none", - } + url = ( + client.app.router["list_projects"] + .url_for() + .with_query( + { + "workspace_id": f"{added_workspace.workspace_id}", + "folder_id": "none", + } + ) ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 # New user list projects in specific folder inside of workspace - base_url = client.app.router["list_projects"].url_for() - url = base_url.with_query( - { - "workspace_id": f"{added_workspace['workspaceId']}", - "folder_id": f"{first_folder['folderId']}", - } + url = ( + client.app.router["list_projects"] + .url_for() + .with_query( + { + "workspace_id": f"{added_workspace.workspace_id}", + "folder_id": f"{first_folder['folderId']}", + } + ) ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 assert data[0]["uuid"] == project["uuid"] @@ -196,10 +213,10 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # New user with write permission creates a folder url = client.app.router["create_folder"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "New user folder", - "workspaceId": f"{added_workspace['workspaceId']}", + "workspaceId": f"{added_workspace.workspace_id}", }, ) await assert_status(resp, status.HTTP_201_CREATED) @@ -207,7 +224,7 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # Now we will remove write permissions await update_or_insert_workspace_group( client.app, - workspace_id=added_workspace["workspaceId"], + workspace_id=added_workspace.workspace_id, group_id=new_logged_user["primary_gid"], read=True, write=False, @@ -217,10 +234,10 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # Now error is raised on the creation of folder as user doesn't have write access url = client.app.router["create_folder"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "New user second folder", - "workspaceId": f"{added_workspace['workspaceId']}", + "workspaceId": f"{added_workspace.workspace_id}", }, ) await assert_status(resp, status.HTTP_403_FORBIDDEN) @@ -228,9 +245,9 @@ async def test_workspaces_full_workflow_with_folders_and_projects( # But user has still read permissions base_url = client.app.router["list_folders"].url_for() url = base_url.with_query( - {"workspace_id": f"{added_workspace['workspaceId']}", "folder_id": "null"} + {"workspace_id": f"{added_workspace.workspace_id}", "folder_id": "null"} ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 2 @@ -271,18 +288,19 @@ async def test_workspaces_delete_folders( # create a new workspace url = client.app.router["create_workspace"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "My first workspace", "description": "Custom description", "thumbnail": None, }, ) - added_workspace, _ = await assert_status(resp, status.HTTP_201_CREATED) + data, _ = await assert_status(resp, status.HTTP_201_CREATED) + added_workspace = WorkspaceGet.parse_obj(data) # Create project in workspace project_data = deepcopy(fake_project) - project_data["workspace_id"] = f"{added_workspace['workspaceId']}" + project_data["workspace_id"] = f"{added_workspace.workspace_id}" first_project = await create_project( client.app, project_data, @@ -297,19 +315,22 @@ async def test_workspaces_delete_folders( ) # List project in workspace - base_url = client.app.router["list_projects"].url_for() - url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + url = ( + client.app.router["list_projects"] + .url_for() + .with_query({"workspace_id": f"{added_workspace.workspace_id}"}) + ) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 2 # Create folder in workspace url = client.app.router["create_folder"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "Original user folder", - "workspaceId": f"{added_workspace['workspaceId']}", + "workspaceId": f"{added_workspace.workspace_id}", }, ) first_folder, _ = await assert_status(resp, status.HTTP_201_CREATED) @@ -317,10 +338,10 @@ async def test_workspaces_delete_folders( # Create sub folder of previous folder url = client.app.router["create_folder"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "Second user folder", - "workspaceId": f"{added_workspace['workspaceId']}", + "workspaceId": f"{added_workspace.workspace_id}", "parentFolderId": f"{first_folder['folderId']}", }, ) @@ -331,7 +352,7 @@ async def test_workspaces_delete_folders( folder_id=f"{first_folder['folderId']}", project_id=f"{first_project['uuid']}", ) - resp = await client.put(url.path) + resp = await client.put(f"{url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) # Move second project in specific folder in workspace @@ -339,19 +360,19 @@ async def test_workspaces_delete_folders( folder_id=f"{second_folder['folderId']}", project_id=f"{second_project['uuid']}", ) - resp = await client.put(url.path) + resp = await client.put(f"{url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) # Delete folder url = client.app.router["delete_folder"].url_for( folder_id=f"{first_folder['folderId']}" ) - resp = await client.delete(url.path) + resp = await client.delete(f"{url}") await assert_status(resp, status.HTTP_204_NO_CONTENT) - fire_and_forget_tasks = client.app[APP_FIRE_AND_FORGET_TASKS_KEY] - t1: asyncio.Task = list(fire_and_forget_tasks)[0] - t2: asyncio.Task = list(fire_and_forget_tasks)[1] + fire_and_forget_tasks = list(client.app[APP_FIRE_AND_FORGET_TASKS_KEY]) + t1: asyncio.Task = fire_and_forget_tasks[0] + t2: asyncio.Task = fire_and_forget_tasks[1] assert t1.get_name().startswith("fire_and_forget_task_delete_project_task_") assert t2.get_name().startswith("fire_and_forget_task_delete_project_task_") await t1 @@ -360,9 +381,12 @@ async def test_workspaces_delete_folders( assert len(client.app[APP_FIRE_AND_FORGET_TASKS_KEY]) == 0 # List project in workspace (The projects should have been deleted) - base_url = client.app.router["list_projects"].url_for() - url = base_url.with_query({"workspace_id": f"{added_workspace['workspaceId']}"}) - resp = await client.get(url) + url = ( + client.app.router["list_projects"] + .url_for() + .with_query({"workspace_id": f"{added_workspace.workspace_id}"}) + ) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 0 @@ -382,7 +406,7 @@ async def test_listing_folders_and_projects_in_workspace__multiple_workspaces_cr # create a new workspace url = client.app.router["create_workspace"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "My first workspace", "description": "Custom description", @@ -404,7 +428,7 @@ async def test_listing_folders_and_projects_in_workspace__multiple_workspaces_cr # Create folder in workspace url = client.app.router["create_folder"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "Original user folder", "workspaceId": f"{added_workspace_1['workspaceId']}", @@ -415,7 +439,7 @@ async def test_listing_folders_and_projects_in_workspace__multiple_workspaces_cr # create a new workspace url = client.app.router["create_workspace"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "My first workspace", "description": "Custom description", @@ -437,7 +461,7 @@ async def test_listing_folders_and_projects_in_workspace__multiple_workspaces_cr # Create folder in workspace url = client.app.router["create_folder"].url_for() resp = await client.post( - url.path, + f"{url}", json={ "name": "Original user folder", "workspaceId": f"{added_workspace_2['workspaceId']}", @@ -446,17 +470,23 @@ async def test_listing_folders_and_projects_in_workspace__multiple_workspaces_cr first_folder, _ = await assert_status(resp, status.HTTP_201_CREATED) # List projects in workspace 1 - base_url = client.app.router["list_projects"].url_for() - url = base_url.with_query({"workspace_id": f"{added_workspace_1['workspaceId']}"}) - resp = await client.get(url) + url = ( + client.app.router["list_projects"] + .url_for() + .with_query({"workspace_id": f"{added_workspace_1['workspaceId']}"}) + ) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 # List folders in workspace 1 - base_url = client.app.router["list_folders"].url_for() - url = base_url.with_query( - {"workspace_id": f"{added_workspace_1['workspaceId']}", "folder_id": "null"} + url = ( + client.app.router["list_folders"] + .url_for() + .with_query( + {"workspace_id": f"{added_workspace_1['workspaceId']}", "folder_id": "null"} + ) ) - resp = await client.get(url) + resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 From 66ef145a4ee520c3f7de926ddabe05702cf00760 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:01:11 +0100 Subject: [PATCH 33/52] workspaces new list --- .../workspaces/_workspaces_db.py | 4 +- .../tests/unit/with_dbs/03/test_trash.py | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index 6c7714829be..7bcc7f7c3a3 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -132,9 +132,9 @@ async def list_workspaces_for_user( if filter_trashed is not None: base_query = base_query.where( - workspaces.c.trashed_at.is_not(None) + workspaces.c.trashed.is_not(None) if filter_trashed - else workspaces.c.trashed_at.is_(None) + else workspaces.c.trashed.is_(None) ) # Select total count from base_query diff --git a/services/web/server/tests/unit/with_dbs/03/test_trash.py b/services/web/server/tests/unit/with_dbs/03/test_trash.py index 7d6c701c522..7a6c3199de9 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_trash.py +++ b/services/web/server/tests/unit/with_dbs/03/test_trash.py @@ -8,6 +8,7 @@ import asyncio from collections.abc import Callable +from typing import AsyncIterable from uuid import UUID import arrow @@ -16,6 +17,7 @@ from aioresponses import aioresponses from models_library.api_schemas_webserver.folders_v2 import FolderGet from models_library.api_schemas_webserver.projects import ProjectGet, ProjectListItem +from models_library.api_schemas_webserver.workspaces import WorkspaceGet from models_library.rest_pagination import Page from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status @@ -395,3 +397,44 @@ async def test_trash_folder_with_content( data, _ = await assert_status(resp, status.HTTP_200_OK) got = ProjectGet.parse_obj(data) assert got.trashed_at is None + + +@pytest.fixture +async def workspace( + client: TestClient, logged_user: UserInfoDict +) -> AsyncIterable[WorkspaceGet]: + + # CREATE a workspace + resp = await client.post("/v0/workspaces", json={"name": "My first workspace"}) + data, _ = await assert_status(resp, status.HTTP_201_CREATED) + workspace = WorkspaceGet.parse_obj(data) + + yield workspace + + # DELETE a workspace + resp = await client.delete(f"/v0/workspaces/{workspace.workspace_id}") + data, _ = await assert_status(resp, status.HTTP_204_NO_CONTENT) + + +@pytest.mark.acceptance_test( + "https://github.com/ITISFoundation/osparc-simcore/pull/6690" +) +async def test_trash_empty_workspace( + client: TestClient, logged_user: UserInfoDict, workspace: WorkspaceGet +): + assert client.app + + # LIST NOT trashed + resp = await client.get("/v0/workspaces") + await assert_status(resp, status.HTTP_200_OK) + + page = Page[WorkspaceGet].parse_obj(await resp.json()) + assert page.meta.total == 1 + assert page.data[0] == workspace + + # LIST trashed + resp = await client.get("/v0/workspaces", params={"filters": '{"trashed": true}'}) + await assert_status(resp, status.HTTP_200_OK) + + page = Page[WorkspaceGet].parse_obj(await resp.json()) + assert page.meta.total == 0 From 61d86e996068f500e5f0ef29f82bd236c8bd41db Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:06:38 +0100 Subject: [PATCH 34/52] drafted tests --- .../tests/unit/with_dbs/03/test_trash.py | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/03/test_trash.py b/services/web/server/tests/unit/with_dbs/03/test_trash.py index 7a6c3199de9..93a73b6ca3e 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_trash.py +++ b/services/web/server/tests/unit/with_dbs/03/test_trash.py @@ -7,8 +7,7 @@ import asyncio -from collections.abc import Callable -from typing import AsyncIterable +from collections.abc import AsyncIterable, Callable from uuid import UUID import arrow @@ -424,7 +423,49 @@ async def test_trash_empty_workspace( ): assert client.app - # LIST NOT trashed + # LIST NOT trashed (default) + resp = await client.get("/v0/workspaces") + await assert_status(resp, status.HTTP_200_OK) + + page = Page[WorkspaceGet].parse_obj(await resp.json()) + assert page.meta.total == 1 + assert page.data[0] == workspace + + # LIST trashed + resp = await client.get("/v0/workspaces", params={"filters": '{"trashed": true}'}) + await assert_status(resp, status.HTTP_200_OK) + + page = Page[WorkspaceGet].parse_obj(await resp.json()) + assert page.meta.total == 0 + + # ------------- + + # TRASH + resp = await client.post(f"/v0/folders/{workspace.workspace_id}:trash") + await assert_status(resp, status.HTTP_204_NO_CONTENT) + + # LIST NOT trashed (default) + resp = await client.get("/v0/workspaces") + await assert_status(resp, status.HTTP_200_OK) + + page = Page[WorkspaceGet].parse_obj(await resp.json()) + assert page.meta.total == 0 + + # LIST trashed + resp = await client.get("/v0/workspaces", params={"filters": '{"trashed": true}'}) + await assert_status(resp, status.HTTP_200_OK) + + page = Page[WorkspaceGet].parse_obj(await resp.json()) + assert page.meta.total == 1 + assert page.data[0] == workspace + + # -------- + + # UN_TRASH + resp = await client.post(f"/v0/workspaces/{workspace.workspace_id}:untrash") + await assert_status(resp, status.HTTP_204_NO_CONTENT) + + # LIST NOT trashed (default) resp = await client.get("/v0/workspaces") await assert_status(resp, status.HTTP_200_OK) From 93edb80ae47b16f0f7606de1b57b6f215f5c7b87 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:37:28 +0100 Subject: [PATCH 35/52] workspace update --- .../src/models_library/workspaces.py | 8 +++ .../workspaces/_trash_api.py | 59 +++++++++++++------ .../workspaces/_workspaces_api.py | 17 +++--- .../workspaces/_workspaces_db.py | 23 ++++---- .../workspaces/_workspaces_handlers.py | 4 +- .../with_dbs/04/workspaces/test_workspaces.py | 4 +- 6 files changed, 73 insertions(+), 42 deletions(-) diff --git a/packages/models-library/src/models_library/workspaces.py b/packages/models-library/src/models_library/workspaces.py index 418397c7dce..9ce1d219d7b 100644 --- a/packages/models-library/src/models_library/workspaces.py +++ b/packages/models-library/src/models_library/workspaces.py @@ -69,3 +69,11 @@ class UserWorkspaceAccessRightsDB(WorkspaceDB): class Config: orm_mode = True + + +class WorkspaceUpdateDB(BaseModel): + name: str | None = None + description: str | None = None + thumbnail: str | None = None + trashed: datetime | None = None + trashed_by: UserID | None = None diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py index c07a35a3091..a77fe579920 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py @@ -6,12 +6,25 @@ from models_library.projects import ProjectID from models_library.users import UserID from models_library.workspaces import WorkspaceID +from simcore_postgres_database.utils_repos import transaction_context +from ..db.plugin import get_asyncpg_engine from ..projects._trash_api import trash_project, untrash_project +from ._workspaces_db import update_workspace _logger = logging.getLogger(__name__) +async def _check_exists_and_access( + app: web.Application, + *, + product_name: ProductName, + user_id: UserID, + workspace_id: WorkspaceID, +) -> bool: + raise NotImplementedError + + async def trash_workspace( app: web.Application, *, @@ -20,33 +33,41 @@ async def trash_workspace( workspace_id: WorkspaceID, force_stop_first: bool, ): - # TODO: Check + await _check_exists_and_access( + app, product_name=product_name, user_id=user_id, workspace_id=workspace_id + ) # Trash trashed_at = arrow.utcnow().datetime + async with transaction_context(get_asyncpg_engine(app)) as connection: - _logger.debug( - "TODO: Unit of work for all workspaces and projects and fails if force_stop_first=%s is False", - force_stop_first, - ) - - # 1. TODO: Trash workspace - - # 2. Trash all child folders - - # 2. Trash all child projects that I am an owner - child_projects: list[ProjectID] = [] - - for project_id in child_projects: - await trash_project( + # EXPLICIT un/trash workspace + await update_workspace( app, + connection, product_name=product_name, - user_id=user_id, - project_id=project_id, - force_stop_first=force_stop_first, - explicit=False, + workspace_id=workspace_id, + ) + _logger.debug( + "TODO: Unit of work for all workspaces and projects and fails if force_stop_first=%s is False", + force_stop_first, ) + # IMPLICIT un/trash child folders and projects + + # 2. Trash all child projects that I am an owner + child_projects: list[ProjectID] = [] + + for project_id in child_projects: + await trash_project( + app, + product_name=product_name, + user_id=user_id, + project_id=project_id, + force_stop_first=force_stop_first, + explicit=False, + ) + async def untrash_workspace( app: web.Application, diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py index 944c2bcfa8d..8ec448f551f 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py @@ -10,7 +10,11 @@ from models_library.products import ProductName from models_library.rest_ordering import OrderBy from models_library.users import UserID -from models_library.workspaces import UserWorkspaceAccessRightsDB, WorkspaceID +from models_library.workspaces import ( + UserWorkspaceAccessRightsDB, + WorkspaceID, + WorkspaceUpdateDB, +) from pydantic import NonNegativeInt from ..projects._db_utils import PermissionStr @@ -110,13 +114,12 @@ async def list_workspaces( async def update_workspace( app: web.Application, *, + product_name: ProductName, user_id: UserID, workspace_id: WorkspaceID, - name: str, - description: str | None, - thumbnail: str | None, - product_name: ProductName, + **updates, ) -> WorkspaceGet: + await check_user_workspace_access( app=app, user_id=user_id, @@ -127,10 +130,8 @@ async def update_workspace( await db.update_workspace( app, workspace_id=workspace_id, - name=name, - description=description, - thumbnail=thumbnail, product_name=product_name, + updates=WorkspaceUpdateDB(**updates), ) workspace_db = await db.get_workspace_for_user( app, diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index 7bcc7f7c3a3..d163605d652 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -15,6 +15,7 @@ UserWorkspaceAccessRightsDB, WorkspaceDB, WorkspaceID, + WorkspaceUpdateDB, ) from pydantic import NonNegativeInt from simcore_postgres_database.models.workspaces import workspaces @@ -51,6 +52,9 @@ ) assert set(WorkspaceDB.__fields__) == {c.name for c in _SELECTION_ARGS} # nosec +assert set(WorkspaceUpdateDB.__fields__).issubset( # nosec + c.name for c in workspaces.columns +) async def create_workspace( @@ -200,21 +204,20 @@ async def update_workspace( app: web.Application, connection: AsyncConnection | None = None, *, - workspace_id: WorkspaceID, - name: str, - description: str | None, - thumbnail: str | None, product_name: ProductName, + workspace_id: WorkspaceID, + updates: WorkspaceUpdateDB, ) -> WorkspaceDB: + # NOTE: at least 'touch' if updated_values is empty + _updates = { + **updates.dict(exclude_unset=True), + "modified": func.now(), + } + async with transaction_context(get_asyncpg_engine(app), connection) as conn: result = await conn.stream( workspaces.update() - .values( - name=name, - description=description, - thumbnail=thumbnail, - modified=func.now(), - ) + .values(**_updates) .where( (workspaces.c.workspace_id == workspace_id) & (workspaces.c.product_name == product_name) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py index 390d7ab87df..1bb56dda0b5 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py @@ -132,10 +132,8 @@ async def replace_workspace(request: web.Request): app=request.app, user_id=req_ctx.user_id, workspace_id=path_params.workspace_id, - name=body_params.name, - description=body_params.description, product_name=req_ctx.product_name, - thumbnail=body_params.thumbnail, + **body_params.dict(), ) return envelope_json_response(workspace) diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py index 7306dfd341b..83d07fcba18 100644 --- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py +++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py @@ -89,7 +89,7 @@ async def test_workspaces_workflow( resp, status.HTTP_200_OK, include_meta=True, include_links=True ) assert len(data) == 1 - assert data[0] == added_workspace.dict() + assert WorkspaceGet.parse_obj(data[0]) == added_workspace assert meta["count"] == 1 assert links @@ -122,7 +122,7 @@ async def test_workspaces_workflow( resp = await client.get(f"{url}") data, _ = await assert_status(resp, status.HTTP_200_OK) assert len(data) == 1 - assert data[0] == replaced_workspace.dict() + assert WorkspaceGet.parse_obj(data[0]) == replaced_workspace # DELETE a workspace url = client.app.router["delete_workspace"].url_for( From eba08778227b47f2cc5916efe663c2a1d40189fd Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:08:39 +0100 Subject: [PATCH 36/52] adds trash empty workspace --- .../workspaces/_trash_api.py | 75 +++++++++++++------ .../workspaces/plugin.py | 3 +- .../tests/unit/with_dbs/03/test_trash.py | 2 +- 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py index a77fe579920..d70ee609652 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py @@ -9,7 +9,11 @@ from simcore_postgres_database.utils_repos import transaction_context from ..db.plugin import get_asyncpg_engine +from ..folders._folders_db import FolderID +from ..folders._trash_api import trash_folder, untrash_folder from ..projects._trash_api import trash_project, untrash_project +from ..workspaces._workspaces_api import WorkspaceUpdateDB +from ._workspaces_api import check_user_workspace_access from ._workspaces_db import update_workspace _logger = logging.getLogger(__name__) @@ -21,8 +25,14 @@ async def _check_exists_and_access( product_name: ProductName, user_id: UserID, workspace_id: WorkspaceID, -) -> bool: - raise NotImplementedError +): + await check_user_workspace_access( + app=app, + user_id=user_id, + workspace_id=workspace_id, + product_name=product_name, + permission="delete", + ) async def trash_workspace( @@ -37,26 +47,31 @@ async def trash_workspace( app, product_name=product_name, user_id=user_id, workspace_id=workspace_id ) - # Trash trashed_at = arrow.utcnow().datetime - async with transaction_context(get_asyncpg_engine(app)) as connection: - # EXPLICIT un/trash workspace + async with transaction_context(get_asyncpg_engine(app)) as connection: + # EXPLICIT trash await update_workspace( app, connection, product_name=product_name, workspace_id=workspace_id, - ) - _logger.debug( - "TODO: Unit of work for all workspaces and projects and fails if force_stop_first=%s is False", - force_stop_first, + updates=WorkspaceUpdateDB(trashed=trashed_at, trashed_by=user_id), ) - # IMPLICIT un/trash child folders and projects + # IMPLICIT trash + child_folders: list[FolderID] = [] # TODO: find children - # 2. Trash all child projects that I am an owner - child_projects: list[ProjectID] = [] + for folder_id in child_folders: + await trash_folder( + app, + product_name=product_name, + user_id=user_id, + folder_id=folder_id, + force_stop_first=force_stop_first, + ) + + child_projects: list[ProjectID] = [] # TODO: find children for project_id in child_projects: await trash_project( @@ -76,15 +91,33 @@ async def untrash_workspace( user_id: UserID, workspace_id: WorkspaceID, ): - # TODO: Check + await _check_exists_and_access( + app, product_name=product_name, user_id=user_id, workspace_id=workspace_id + ) + + async with transaction_context(get_asyncpg_engine(app)) as connection: + # EXPLICIT UNtrash + await update_workspace( + app, + connection, + product_name=product_name, + workspace_id=workspace_id, + updates=WorkspaceUpdateDB(trashed=None, trashed_by=None), + ) - # 3. UNtrash + child_folders: list[FolderID] = [] # TODO: find children - # 3.1 UNtrash workspace and children + for folder_id in child_folders: + await untrash_folder( + app, + product_name=product_name, + user_id=user_id, + folder_id=folder_id, + ) - # 3.2 UNtrash all child projects that I am an owner - child_projects: list[ProjectID] = [] - for project_id in child_projects: - await untrash_project( - app, product_name=product_name, user_id=user_id, project_id=project_id - ) + child_projects: list[ProjectID] = [] # TODO: find children + + for project_id in child_projects: + await untrash_project( + app, product_name=product_name, user_id=user_id, project_id=project_id + ) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/plugin.py b/services/web/server/src/simcore_service_webserver/workspaces/plugin.py index 4773b056b49..d67a9167c92 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/plugin.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/plugin.py @@ -7,7 +7,7 @@ from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup -from . import _groups_handlers, _workspaces_handlers +from . import _groups_handlers, _trash_handlers, _workspaces_handlers _logger = logging.getLogger(__name__) @@ -25,3 +25,4 @@ def setup_workspaces(app: web.Application): # routes app.router.add_routes(_workspaces_handlers.routes) app.router.add_routes(_groups_handlers.routes) + app.router.add_routes(_trash_handlers.routes) diff --git a/services/web/server/tests/unit/with_dbs/03/test_trash.py b/services/web/server/tests/unit/with_dbs/03/test_trash.py index 93a73b6ca3e..5a8b14b4964 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_trash.py +++ b/services/web/server/tests/unit/with_dbs/03/test_trash.py @@ -441,7 +441,7 @@ async def test_trash_empty_workspace( # ------------- # TRASH - resp = await client.post(f"/v0/folders/{workspace.workspace_id}:trash") + resp = await client.post(f"/v0/workspaces/{workspace.workspace_id}:trash") await assert_status(resp, status.HTTP_204_NO_CONTENT) # LIST NOT trashed (default) From f4b47acb77226b7b68573d87c6dac96c3121fcfd Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:23:03 +0100 Subject: [PATCH 37/52] fixes tests --- .../workspaces/_trash_handlers.py | 4 ++-- .../workspaces/_workspaces_api.py | 7 ++++++- .../tests/unit/with_dbs/03/test_trash.py | 20 +++++++++++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py index d245cf0ac42..b2d3a4b41a8 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_handlers.py @@ -25,7 +25,7 @@ @routes.post(f"/{VTAG}/workspaces/{{workspace_id}}:trash", name="trash_workspace") @requires_dev_feature_enabled @login_required -@permission_required("workspace.delete") +@permission_required("workspaces.*") @handle_plugin_requests_exceptions async def trash_workspace(request: web.Request): user_id = get_user_id(request) @@ -49,7 +49,7 @@ async def trash_workspace(request: web.Request): @routes.post(f"/{VTAG}/workspaces/{{workspace_id}}:untrash", name="untrash_workspace") @requires_dev_feature_enabled @login_required -@permission_required("workspace.delete") +@permission_required("workspaces.*") @handle_plugin_requests_exceptions async def untrash_workspace(request: web.Request): user_id = get_user_id(request) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py index 8ec448f551f..05122e2754f 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py @@ -175,5 +175,10 @@ async def check_user_workspace_access( app=app, user_id=user_id, workspace_id=workspace_id, product_name=product_name ) if getattr(workspace_db.my_access_rights, permission, False) is False: - raise WorkspaceAccessForbiddenError + raise WorkspaceAccessForbiddenError( + user_id=user_id, + workspace_id=workspace_id, + product_name=product_name, + permission_checked=permission, + ) return workspace_db diff --git a/services/web/server/tests/unit/with_dbs/03/test_trash.py b/services/web/server/tests/unit/with_dbs/03/test_trash.py index 5a8b14b4964..b51a60207b7 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_trash.py +++ b/services/web/server/tests/unit/with_dbs/03/test_trash.py @@ -423,6 +423,9 @@ async def test_trash_empty_workspace( ): assert client.app + assert workspace.trashed_at is None + assert workspace.trashed_by is None + # LIST NOT trashed (default) resp = await client.get("/v0/workspaces") await assert_status(resp, status.HTTP_200_OK) @@ -440,7 +443,10 @@ async def test_trash_empty_workspace( # ------------- + _exclude_attrs = {"trashed_by", "trashed_at", "modified_at"} + # TRASH + before_trash = arrow.utcnow().datetime resp = await client.post(f"/v0/workspaces/{workspace.workspace_id}:trash") await assert_status(resp, status.HTTP_204_NO_CONTENT) @@ -457,7 +463,12 @@ async def test_trash_empty_workspace( page = Page[WorkspaceGet].parse_obj(await resp.json()) assert page.meta.total == 1 - assert page.data[0] == workspace + assert page.data[0].dict(exclude=_exclude_attrs) == workspace.dict( + exclude=_exclude_attrs + ) + assert page.data[0].trashed_at is not None + assert before_trash < page.data[0].trashed_at + assert page.data[0].trashed_by == logged_user["id"] # -------- @@ -471,7 +482,12 @@ async def test_trash_empty_workspace( page = Page[WorkspaceGet].parse_obj(await resp.json()) assert page.meta.total == 1 - assert page.data[0] == workspace + assert page.data[0].dict(exclude=_exclude_attrs) == workspace.dict( + exclude=_exclude_attrs + ) + + assert page.data[0].trashed_at is None + assert page.data[0].trashed_by is None # LIST trashed resp = await client.get("/v0/workspaces", params={"filters": '{"trashed": true}'}) From fee23e5138d890c5ce0f6f6351ff24cbe3fb4fab Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:24:45 +0100 Subject: [PATCH 38/52] fixes mypy --- .../src/simcore_service_webserver/workspaces/_trash_api.py | 5 ++--- .../simcore_service_webserver/workspaces/_workspaces_api.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py index d70ee609652..ea1c4110859 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py @@ -2,17 +2,16 @@ import arrow from aiohttp import web +from models_library.folders import FolderID from models_library.products import ProductName from models_library.projects import ProjectID from models_library.users import UserID -from models_library.workspaces import WorkspaceID +from models_library.workspaces import WorkspaceID, WorkspaceUpdateDB from simcore_postgres_database.utils_repos import transaction_context from ..db.plugin import get_asyncpg_engine -from ..folders._folders_db import FolderID from ..folders._trash_api import trash_folder, untrash_folder from ..projects._trash_api import trash_project, untrash_project -from ..workspaces._workspaces_api import WorkspaceUpdateDB from ._workspaces_api import check_user_workspace_access from ._workspaces_db import update_workspace diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py index 05122e2754f..3fd6633bb06 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_api.py @@ -25,7 +25,7 @@ _logger = logging.getLogger(__name__) -def _to_api_model(workspace_db: UserWorkspaceAccessRightsDB): +def _to_api_model(workspace_db: UserWorkspaceAccessRightsDB) -> WorkspaceGet: return WorkspaceGet( workspace_id=workspace_db.workspace_id, name=workspace_db.name, From be918740593d57b7f2143bb5e67cf4c320d4790e Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:25:43 +0100 Subject: [PATCH 39/52] update OAS --- .../src/simcore_service_webserver/api/v0/openapi.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 c6ec3af803b..0715404fd7e 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 @@ -3158,7 +3158,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"type","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -3381,7 +3381,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"type","direction":"desc"}' + example: '{"field":"name","direction":"desc"}' name: order_by in: query - required: false @@ -4389,7 +4389,7 @@ paths: '403': description: ProjectInvalidRightsError '404': - description: UserDefaultWalletNotFoundError, ProjectNotFoundError + description: ProjectNotFoundError, UserDefaultWalletNotFoundError '409': description: ProjectTooManyProjectOpenedError '422': @@ -4612,7 +4612,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"root_parent_project_id","direction":"desc"}' + example: '{"field":"user_id","direction":"desc"}' name: order_by in: query - required: false @@ -4726,7 +4726,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"root_parent_project_id","direction":"desc"}' + example: '{"field":"user_id","direction":"desc"}' name: order_by in: query - required: false From 482306124d0c13ee124fe11f7de0630ad719c9ee Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:32:05 +0100 Subject: [PATCH 40/52] sonar --- .../models/_common.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/_common.py b/packages/postgres-database/src/simcore_postgres_database/models/_common.py index 88cab05ed1b..0bcc268e5c4 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/_common.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/_common.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Final import sqlalchemy as sa @@ -5,6 +6,15 @@ from ..constants import DECIMAL_PLACES +class ReferentialAction(str, Enum): + # SEE https://docs.sqlalchemy.org/en/20/orm/cascades.html + CASCADE = "CASCADE" + SET_NULL = "SET NULL" + SET_DEFAULT = "SET DEFAULT" + RESTRICT = "RESTRICT" + NO_ACTION = "NO ACTION" + + def column_created_datetime(*, timezone: bool = True) -> sa.Column: return sa.Column( "created", @@ -34,8 +44,8 @@ def column_created_by_user( sa.Integer, sa.ForeignKey( users_table.c.id, - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=ReferentialAction.CASCADE, + ondelete=ReferentialAction.SET_NULL, ), nullable=not required, doc="Who created this row at `created`", @@ -50,8 +60,8 @@ def column_modified_by_user( sa.Integer, sa.ForeignKey( users_table.c.id, - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=ReferentialAction.CASCADE, + ondelete=ReferentialAction.SET_NULL, ), nullable=not required, doc="Who modified this row at `modified`", @@ -74,8 +84,8 @@ def column_trashed_by_user(resource_name: str, users_table: sa.Table) -> sa.Colu sa.BigInteger, sa.ForeignKey( users_table.c.id, - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=ReferentialAction.CASCADE, + ondelete=ReferentialAction.SET_NULL, ), nullable=True, comment=f"User who trashed the {resource_name}, or null if not trashed or user is unknown.", From a2a466ccb3a23cd1c1a6dfbe3ab67e04574d8aa8 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:43:23 +0100 Subject: [PATCH 41/52] fixes migration --- ...y => c9db8bf5091e_trash_columns_in_workspaces.py} | 12 +++++++----- .../src/simcore_postgres_database/models/_common.py | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) rename packages/postgres-database/src/simcore_postgres_database/migration/versions/{aff5aa99ee3c_trash_columns_in_workspaces.py => c9db8bf5091e_trash_columns_in_workspaces.py} (83%) diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/aff5aa99ee3c_trash_columns_in_workspaces.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/c9db8bf5091e_trash_columns_in_workspaces.py similarity index 83% rename from packages/postgres-database/src/simcore_postgres_database/migration/versions/aff5aa99ee3c_trash_columns_in_workspaces.py rename to packages/postgres-database/src/simcore_postgres_database/migration/versions/c9db8bf5091e_trash_columns_in_workspaces.py index 4916727de56..b61a9e21009 100644 --- a/packages/postgres-database/src/simcore_postgres_database/migration/versions/aff5aa99ee3c_trash_columns_in_workspaces.py +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/c9db8bf5091e_trash_columns_in_workspaces.py @@ -1,15 +1,15 @@ """trash columns in workspaces -Revision ID: aff5aa99ee3c +Revision ID: c9db8bf5091e Revises: 8e1f83486be7 -Create Date: 2024-11-20 13:27:26.677261+00:00 +Create Date: 2024-11-20 16:42:43.784855+00:00 """ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = "aff5aa99ee3c" +revision = "c9db8bf5091e" down_revision = "8e1f83486be7" branch_labels = None depends_on = None @@ -36,7 +36,7 @@ def upgrade(): ), ) op.create_foreign_key( - None, + "fk_workspace_trashed_by_user_id", "workspaces", "users", ["trashed_by"], @@ -49,7 +49,9 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, "workspaces", type_="foreignkey") + op.drop_constraint( + "fk_workspace_trashed_by_user_id", "workspaces", type_="foreignkey" + ) op.drop_column("workspaces", "trashed_by") op.drop_column("workspaces", "trashed") # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/models/_common.py b/packages/postgres-database/src/simcore_postgres_database/models/_common.py index 0bcc268e5c4..2cf69ed544a 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/_common.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/_common.py @@ -1,4 +1,3 @@ -from enum import Enum from typing import Final import sqlalchemy as sa @@ -6,7 +5,7 @@ from ..constants import DECIMAL_PLACES -class ReferentialAction(str, Enum): +class ReferentialAction: # SEE https://docs.sqlalchemy.org/en/20/orm/cascades.html CASCADE = "CASCADE" SET_NULL = "SET NULL" @@ -86,6 +85,7 @@ def column_trashed_by_user(resource_name: str, users_table: sa.Table) -> sa.Colu users_table.c.id, onupdate=ReferentialAction.CASCADE, ondelete=ReferentialAction.SET_NULL, + name=f"fk_{resource_name}_trashed_by_user_id", ), nullable=True, comment=f"User who trashed the {resource_name}, or null if not trashed or user is unknown.", From 2a60b998eefe4f0906c29b04ae5ba5b2a273350c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:46:53 +0100 Subject: [PATCH 42/52] undos --- packages/models-library/src/models_library/utils/enums.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/models-library/src/models_library/utils/enums.py b/packages/models-library/src/models_library/utils/enums.py index d0f3aa61f4e..38afdac0de1 100644 --- a/packages/models-library/src/models_library/utils/enums.py +++ b/packages/models-library/src/models_library/utils/enums.py @@ -6,9 +6,7 @@ @unique class StrAutoEnum(StrEnum): @staticmethod - def _generate_next_value_( - name: str, start: str, count: int, last_values: int - ) -> str: + def _generate_next_value_(name: str, start, count, last_values): return name.upper() From dbba633e22afd3db6ba44d97bc151afa2d897cd8 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:55:21 +0100 Subject: [PATCH 43/52] readme --- services/web/server/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/web/server/README.md b/services/web/server/README.md index c41841a85d2..05a35d97382 100644 --- a/services/web/server/README.md +++ b/services/web/server/README.md @@ -1,10 +1,8 @@ # web/server [![ReDoc](https://img.shields.io/badge/OpenAPI-ReDoc-85ea2d?logo=openapiinitiative)](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/ITISFoundation/osparc-simcore/refs/heads/master/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml) +[![Swagger UI](https://img.shields.io/badge/OpenAPI-Swagger_UI-85ea2d?logo=swagger)](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/ITISFoundation/osparc-simcore/refs/heads/master/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml) [![Docker Pulls](https://img.shields.io/docker/pulls/itisfoundation/webserver.svg)](https://hub.docker.com/r/itisfoundation/webserver/tags) -[![](https://images.microbadger.com/badges/image/itisfoundation/webserver.svg)](https://microbadger.com/images/itisfoundation/webserver "More on service image in registry") -[![](https://images.microbadger.com/badges/version/itisfoundation/webserver.svg)](https://microbadger.com/images/itisfoundation/webserver "More on service image in registry") -[![](https://images.microbadger.com/badges/commit/itisfoundation/webserver.svg)](https://microbadger.com/images/itisfoundation/webserver "More on service image in registry") Corresponds to the ```webserver``` service (see all services in ``services/docker-compose.yml``) From af843e5e8781f0d8272dd3a8eb5cb9418bf7da9a Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:57:57 +0100 Subject: [PATCH 44/52] udno --- packages/models-library/src/models_library/utils/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/models-library/src/models_library/utils/enums.py b/packages/models-library/src/models_library/utils/enums.py index 38afdac0de1..7f0ff7eaf48 100644 --- a/packages/models-library/src/models_library/utils/enums.py +++ b/packages/models-library/src/models_library/utils/enums.py @@ -6,7 +6,7 @@ @unique class StrAutoEnum(StrEnum): @staticmethod - def _generate_next_value_(name: str, start, count, last_values): + def _generate_next_value_(name, start, count, last_values): return name.upper() From 018ad5840fc20e9d93affaad4477348e2287cae8 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:11:16 +0100 Subject: [PATCH 45/52] path --- api/specs/web-server/_trash.py | 4 ++-- .../simcore_service_webserver/api/v0/openapi.yaml | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py index 06411ed38e5..6eb39f6593c 100644 --- a/api/specs/web-server/_trash.py +++ b/api/specs/web-server/_trash.py @@ -104,7 +104,7 @@ def untrash_folder( @router.post( - "/workspace/{workspace_id}:trash", + "/workspaces/{workspace_id}:trash", tags=_extra_tags, status_code=status.HTTP_204_NO_CONTENT, responses={ @@ -123,7 +123,7 @@ def trash_workspace( @router.post( - "/workspace/{workspace_id}:untrash", + "/workspaces/{workspace_id}:untrash", tags=_extra_tags, status_code=status.HTTP_204_NO_CONTENT, ) 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 0715404fd7e..bd16cb41336 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 @@ -3158,7 +3158,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' + example: '{"field":"description","direction":"desc"}' name: order_by in: query - required: false @@ -3381,7 +3381,7 @@ paths: description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`) and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`. default: '{"field":"last_change_date","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' + example: '{"field":"description","direction":"desc"}' name: order_by in: query - required: false @@ -4389,7 +4389,7 @@ paths: '403': description: ProjectInvalidRightsError '404': - description: ProjectNotFoundError, UserDefaultWalletNotFoundError + description: UserDefaultWalletNotFoundError, ProjectNotFoundError '409': description: ProjectTooManyProjectOpenedError '422': @@ -4612,7 +4612,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"user_id","direction":"desc"}' + example: '{"field":"node_id","direction":"desc"}' name: order_by in: query - required: false @@ -4726,7 +4726,7 @@ paths: description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`) and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`. default: '{"field":"started_at","direction":"desc"}' - example: '{"field":"user_id","direction":"desc"}' + example: '{"field":"node_id","direction":"desc"}' name: order_by in: query - required: false @@ -5523,7 +5523,7 @@ paths: responses: '204': description: Successful Response - /v0/workspace/{workspace_id}:trash: + /v0/workspaces/{workspace_id}:trash: post: tags: - trash @@ -5556,7 +5556,7 @@ paths: be trashed '503': description: Trash service error - /v0/workspace/{workspace_id}:untrash: + /v0/workspaces/{workspace_id}:untrash: post: tags: - trash From 5d30429366c1602e434ff60da1bd9ba49500a824 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:17:03 +0100 Subject: [PATCH 46/52] cleanup --- .../folders/_exceptions_handlers.py | 3 ++- .../workspaces/_exceptions_handlers.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/folders/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/folders/_exceptions_handlers.py index 4f83b5e1872..c611809decd 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/folders/_exceptions_handlers.py @@ -57,13 +57,14 @@ status.HTTP_409_CONFLICT, "Invalid folder value set: {reason}", ), + # Trashing ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, "One or more studies in this folder are in use and cannot be trashed. Please stop all services first and try again", ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "Something went wrong while stopping services before trashing. Aborting trash.", + "Something went wrong while stopping running services in studies within this folder before trashing. Aborting trash.", ), } diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py index aa59bd6a036..f418e814169 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py @@ -7,6 +7,7 @@ HttpErrorInfo, create_exception_handlers_decorator, ) +from ..projects.exceptions import ProjectRunningConflictError, ProjectStoppingError from .errors import ( WorkspaceAccessForbiddenError, WorkspaceGroupNotFoundError, @@ -30,6 +31,15 @@ status.HTTP_404_NOT_FOUND, "Workspace not found. {reason}", ), + # Trashing + ProjectRunningConflictError: HttpErrorInfo( + status.HTTP_409_CONFLICT, + "One or more studies in this workspace are in use and cannot be trashed. Please stop all services first and try again", + ), + ProjectStoppingError: HttpErrorInfo( + status.HTTP_503_SERVICE_UNAVAILABLE, + "Something went wrong while stopping running services in studies within this workspace before trashing. Aborting trash.", + ), } From 567075ec440273ebb60fd69653588b20367b90c3 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:44:39 +0100 Subject: [PATCH 47/52] rm badges --- services/web/server/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/web/server/README.md b/services/web/server/README.md index 05a35d97382..4443c6e909a 100644 --- a/services/web/server/README.md +++ b/services/web/server/README.md @@ -1,9 +1,5 @@ # web/server -[![ReDoc](https://img.shields.io/badge/OpenAPI-ReDoc-85ea2d?logo=openapiinitiative)](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/ITISFoundation/osparc-simcore/refs/heads/master/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml) -[![Swagger UI](https://img.shields.io/badge/OpenAPI-Swagger_UI-85ea2d?logo=swagger)](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/ITISFoundation/osparc-simcore/refs/heads/master/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml) -[![Docker Pulls](https://img.shields.io/docker/pulls/itisfoundation/webserver.svg)](https://hub.docker.com/r/itisfoundation/webserver/tags) - Corresponds to the ```webserver``` service (see all services in ``services/docker-compose.yml``) From 75964f9a0f882024105a79bc2c8105b7fb21afce Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:48:53 +0100 Subject: [PATCH 48/52] fixes testss --- .../workspaces/_exceptions_handlers.py | 8 ++++++-- .../simcore_service_webserver/workspaces/_trash_api.py | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py index f418e814169..f6470f461f7 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_exceptions_handlers.py @@ -7,7 +7,11 @@ HttpErrorInfo, create_exception_handlers_decorator, ) -from ..projects.exceptions import ProjectRunningConflictError, ProjectStoppingError +from ..projects.exceptions import ( + BaseProjectError, + ProjectRunningConflictError, + ProjectStoppingError, +) from .errors import ( WorkspaceAccessForbiddenError, WorkspaceGroupNotFoundError, @@ -44,6 +48,6 @@ handle_plugin_requests_exceptions = create_exception_handlers_decorator( - exceptions_catch=(WorkspacesValueError), + exceptions_catch=(BaseProjectError, WorkspacesValueError), exc_to_status_map=_TO_HTTP_ERROR_MAP, ) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py index ea1c4110859..18c3ae93b88 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_trash_api.py @@ -59,7 +59,7 @@ async def trash_workspace( ) # IMPLICIT trash - child_folders: list[FolderID] = [] # TODO: find children + child_folders: list[FolderID] = [] # TODO: find children. Check with MD for folder_id in child_folders: await trash_folder( @@ -70,7 +70,7 @@ async def trash_workspace( force_stop_first=force_stop_first, ) - child_projects: list[ProjectID] = [] # TODO: find children + child_projects: list[ProjectID] = [] # TODO: find children. Check with MD for project_id in child_projects: await trash_project( @@ -104,7 +104,7 @@ async def untrash_workspace( updates=WorkspaceUpdateDB(trashed=None, trashed_by=None), ) - child_folders: list[FolderID] = [] # TODO: find children + child_folders: list[FolderID] = [] # TODO: find children. Check with MD for folder_id in child_folders: await untrash_folder( @@ -114,7 +114,7 @@ async def untrash_workspace( folder_id=folder_id, ) - child_projects: list[ProjectID] = [] # TODO: find children + child_projects: list[ProjectID] = [] # TODO: find children. Check with MD for project_id in child_projects: await untrash_project( From b20528b297b3faa3ba81f7458e0b05622d6f426f Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:33:24 +0100 Subject: [PATCH 49/52] minor --- .../simcore_service_webserver/workspaces/_workspaces_db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index 1907a6dd0c6..7c55e0a9428 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -51,8 +51,8 @@ workspaces.c.trashed_by, ) -assert set(WorkspaceDB.__fields__) == {c.name for c in _SELECTION_ARGS} # nosec -assert set(WorkspaceUpdateDB.__fields__).issubset( # nosec +assert set(WorkspaceDB.model_fields) == {c.name for c in _SELECTION_ARGS} # nosec +assert set(WorkspaceUpdateDB.model_fields).issubset( # nosec c.name for c in workspaces.columns ) From 737f7ae1ae43c30430d370f29222b36f2493b45c Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:19:32 +0100 Subject: [PATCH 50/52] updates pg --- .../models/_common.py | 28 ++++++------- .../models/api_keys.py | 7 ++-- .../models/classifiers.py | 5 ++- .../models/cluster_to_groups.py | 9 +++-- .../models/clusters.py | 5 ++- .../models/comp_runs.py | 13 ++++--- .../models/confirmations.py | 6 ++- .../models/folders_v2.py | 16 ++++---- .../models/groups.py | 9 +++-- .../models/groups_extra_properties.py | 14 +++---- .../models/payments_autorecharge.py | 5 ++- .../models/products.py | 9 +++-- .../models/products_prices.py | 6 +-- .../models/products_to_templates.py | 9 +++-- .../models/project_to_groups.py | 10 ++--- .../models/projects.py | 9 +++-- .../models/projects_comments.py | 8 ++-- .../models/projects_metadata.py | 13 ++++--- .../models/projects_networks.py | 5 ++- .../models/projects_node_to_pricing_unit.py | 5 ++- .../models/projects_nodes.py | 5 ++- .../models/projects_tags.py | 9 +++-- .../models/projects_to_folders.py | 14 +++---- .../models/projects_to_products.py | 10 ++--- .../models/projects_to_wallet.py | 10 ++--- .../models/projects_version_control.py | 39 ++++++++++--------- .../resource_tracker_credit_transactions.py | 11 ++++-- ...esource_tracker_pricing_plan_to_service.py | 10 ++--- .../models/resource_tracker_pricing_plans.py | 6 +-- .../resource_tracker_pricing_unit_costs.py | 15 ++++--- .../models/resource_tracker_pricing_units.py | 6 +-- .../models/services.py | 17 ++++---- .../models/services_compatibility.py | 5 ++- .../models/services_consume_filetypes.py | 5 ++- .../models/services_environments.py | 10 ++--- .../models/services_specifications.py | 9 +++-- .../models/services_tags.py | 9 +++-- .../models/tags_access_rights.py | 10 ++--- .../models/user_preferences.py | 9 +++-- .../models/user_to_projects.py | 9 +++-- .../simcore_postgres_database/models/users.py | 5 ++- .../models/users_details.py | 9 +++-- .../models/wallet_to_groups.py | 10 ++--- .../models/wallets.py | 10 ++--- .../models/workspaces.py | 9 +++-- .../models/workspaces_access_rights.py | 10 ++--- .../tests/test_models_tags.py | 4 +- 47 files changed, 256 insertions(+), 210 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/_common.py b/packages/postgres-database/src/simcore_postgres_database/models/_common.py index 2cf69ed544a..47bfeb6ebf0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/_common.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/_common.py @@ -5,13 +5,15 @@ from ..constants import DECIMAL_PLACES -class ReferentialAction: - # SEE https://docs.sqlalchemy.org/en/20/orm/cascades.html - CASCADE = "CASCADE" - SET_NULL = "SET NULL" - SET_DEFAULT = "SET DEFAULT" - RESTRICT = "RESTRICT" - NO_ACTION = "NO ACTION" +class RefActions: + """Referential actions for `ON UPDATE`, `ON DELETE`""" + + # SEE https://docs.sqlalchemy.org/en/20/core/constraints.html#on-update-on-delete + CASCADE: Final[str] = "CASCADE" + SET_NULL: Final[str] = "SET NULL" + SET_DEFAULT: Final[str] = "SET DEFAULT" + RESTRICT: Final[str] = "RESTRICT" + NO_ACTION: Final[str] = "NO ACTION" def column_created_datetime(*, timezone: bool = True) -> sa.Column: @@ -43,8 +45,8 @@ def column_created_by_user( sa.Integer, sa.ForeignKey( users_table.c.id, - onupdate=ReferentialAction.CASCADE, - ondelete=ReferentialAction.SET_NULL, + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, ), nullable=not required, doc="Who created this row at `created`", @@ -59,8 +61,8 @@ def column_modified_by_user( sa.Integer, sa.ForeignKey( users_table.c.id, - onupdate=ReferentialAction.CASCADE, - ondelete=ReferentialAction.SET_NULL, + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, ), nullable=not required, doc="Who modified this row at `modified`", @@ -83,8 +85,8 @@ def column_trashed_by_user(resource_name: str, users_table: sa.Table) -> sa.Colu sa.BigInteger, sa.ForeignKey( users_table.c.id, - onupdate=ReferentialAction.CASCADE, - ondelete=ReferentialAction.SET_NULL, + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, name=f"fk_{resource_name}_trashed_by_user_id", ), nullable=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py b/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py index 5db5416b677..416e29d4e2d 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/api_keys.py @@ -13,6 +13,7 @@ import sqlalchemy as sa from sqlalchemy.sql import func +from ._common import RefActions from .base import metadata from .users import users @@ -35,7 +36,7 @@ sa.Column( "user_id", sa.BigInteger(), - sa.ForeignKey(users.c.id, ondelete="CASCADE"), + sa.ForeignKey(users.c.id, ondelete=RefActions.CASCADE), nullable=False, doc="Identified user", ), @@ -44,8 +45,8 @@ sa.String, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_api_keys_product_name", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/classifiers.py b/packages/postgres-database/src/simcore_postgres_database/models/classifiers.py index 238c7b04f8c..7e4a2cf39df 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/classifiers.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/classifiers.py @@ -11,6 +11,7 @@ from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.sql import func +from ._common import RefActions from .base import metadata group_classifiers = sa.Table( @@ -32,8 +33,8 @@ sa.ForeignKey( "groups.gid", name="fk_group_classifiers_gid_to_groups_gid", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), unique=True, # Every Group can ONLY have one set of classifiers ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/cluster_to_groups.py b/packages/postgres-database/src/simcore_postgres_database/models/cluster_to_groups.py index 6f563926200..63996c1404b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/cluster_to_groups.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/cluster_to_groups.py @@ -1,6 +1,7 @@ import sqlalchemy as sa from sqlalchemy.sql import expression, func +from ._common import RefActions from .base import metadata from .clusters import clusters from .groups import groups @@ -14,8 +15,8 @@ sa.ForeignKey( clusters.c.id, name="fk_cluster_to_groups_id_clusters", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Cluster unique ID", ), @@ -25,8 +26,8 @@ sa.ForeignKey( groups.c.gid, name="fk_cluster_to_groups_gid_groups", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Group unique IDentifier", ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/clusters.py b/packages/postgres-database/src/simcore_postgres_database/models/clusters.py index a4ddc53123c..39536ae241b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/clusters.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/clusters.py @@ -4,6 +4,7 @@ from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.sql import func +from ._common import RefActions from .base import metadata @@ -36,8 +37,8 @@ class ClusterType(Enum): sa.ForeignKey( "groups.gid", name="fk_clusters_gid_groups", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), nullable=False, doc="Identifier of the group that owns this cluster", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/comp_runs.py b/packages/postgres-database/src/simcore_postgres_database/models/comp_runs.py index eb84cefaa76..3975cb91eee 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/comp_runs.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/comp_runs.py @@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.sql import func +from ._common import RefActions from .base import metadata from .comp_pipeline import StateType @@ -26,8 +27,8 @@ sa.ForeignKey( "projects.uuid", name="fk_comp_runs_project_uuid_projects", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=False, doc="The project uuid with which the run entry is associated", @@ -38,8 +39,8 @@ sa.ForeignKey( "users.id", name="fk_comp_runs_user_id_users", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=False, doc="The user id with which the run entry is associated", @@ -50,8 +51,8 @@ sa.ForeignKey( "clusters.id", name="fk_comp_runs_cluster_id_clusters", - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, ), nullable=True, doc="The cluster id on which the run entry is associated, if NULL or 0 uses the default", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py b/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py index 3b9b1274804..6fd56e8c8e0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/confirmations.py @@ -9,6 +9,7 @@ import sqlalchemy as sa +from ._common import RefActions from .base import metadata from .users import users @@ -57,6 +58,9 @@ class ConfirmationAction(enum.Enum): # constraints ---------------- sa.PrimaryKeyConstraint("code", name="confirmation_code"), sa.ForeignKeyConstraint( - ["user_id"], [users.c.id], name="user_confirmation_fkey", ondelete="CASCADE" + ["user_id"], + [users.c.id], + name="user_confirmation_fkey", + ondelete=RefActions.CASCADE, ), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py b/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py index fcad0ada76c..78f3de8bdf9 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/folders_v2.py @@ -1,7 +1,7 @@ import sqlalchemy as sa from sqlalchemy.sql import expression -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .workspaces import workspaces @@ -35,8 +35,8 @@ sa.String, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_new_folders_to_products_name", ), nullable=False, @@ -46,8 +46,8 @@ sa.BigInteger, sa.ForeignKey( "users.id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_folders_to_user_id", ), nullable=True, @@ -57,8 +57,8 @@ sa.BigInteger, sa.ForeignKey( workspaces.c.workspace_id, - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_folders_to_workspace_id", ), nullable=True, @@ -69,7 +69,7 @@ sa.ForeignKey( "groups.gid", name="fk_new_folders_to_groups_gid", - ondelete="SET NULL", + ondelete=RefActions.SET_NULL, ), nullable=True, ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/groups.py b/packages/postgres-database/src/simcore_postgres_database/models/groups.py index 0aec758a6c6..a70e9fa8db4 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/groups.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/groups.py @@ -10,6 +10,7 @@ from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.sql import func +from ._common import RefActions from .base import metadata @@ -86,8 +87,8 @@ class GroupType(enum.Enum): sa.ForeignKey( "users.id", name="fk_user_to_groups_id_users", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="User unique IDentifier", ), @@ -97,8 +98,8 @@ class GroupType(enum.Enum): sa.ForeignKey( "groups.gid", name="fk_user_to_groups_gid_groups", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Group unique IDentifier", ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/groups_extra_properties.py b/packages/postgres-database/src/simcore_postgres_database/models/groups_extra_properties.py index 93ffe8cd7f7..e25a1bd3b2b 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/groups_extra_properties.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/groups_extra_properties.py @@ -1,21 +1,19 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata -# -# groups_extra_properties: Maps internet access permissions to groups -# groups_extra_properties = sa.Table( "groups_extra_properties", + # groups_extra_properties: Maps internet access permissions to groups metadata, sa.Column( "group_id", sa.BigInteger, sa.ForeignKey( "groups.gid", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_groups_extra_properties_to_group_group_id", ), nullable=False, @@ -26,8 +24,8 @@ sa.VARCHAR, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_groups_extra_properties_to_products_name", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py b/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py index 3b46e217fcc..df30251c50c 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/payments_autorecharge.py @@ -2,6 +2,7 @@ from ._common import ( NUMERIC_KWARGS, + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, @@ -49,8 +50,8 @@ sa.ForeignKey( payments_methods.c.payment_method_id, name="fk_payments_autorecharge_primary_payment_method_id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=False, unique=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/products.py b/packages/postgres-database/src/simcore_postgres_database/models/products.py index 70ed22911d7..344326fbf48 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/products.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/products.py @@ -15,6 +15,7 @@ TypedDict, ) +from ._common import RefActions from .base import metadata from .groups import groups from .jinja2_templates import jinja2_templates @@ -199,8 +200,8 @@ class ProductLoginSettingsDict(TypedDict, total=False): sa.ForeignKey( jinja2_templates.c.name, name="fk_jinja2_templates_name", - ondelete="SET NULL", - onupdate="CASCADE", + ondelete=RefActions.SET_NULL, + onupdate=RefActions.CASCADE, ), nullable=True, doc="Custom jinja2 template for registration email", @@ -238,8 +239,8 @@ class ProductLoginSettingsDict(TypedDict, total=False): sa.ForeignKey( groups.c.gid, name="fk_products_group_id", - ondelete="SET NULL", - onupdate="CASCADE", + ondelete=RefActions.SET_NULL, + onupdate=RefActions.CASCADE, ), unique=True, nullable=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/products_prices.py b/packages/postgres-database/src/simcore_postgres_database/models/products_prices.py index 2784969149b..b0b652cd310 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/products_prices.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/products_prices.py @@ -1,6 +1,6 @@ import sqlalchemy as sa -from ._common import NUMERIC_KWARGS +from ._common import NUMERIC_KWARGS, RefActions from .base import metadata from .products import products @@ -20,8 +20,8 @@ sa.ForeignKey( products.c.name, name="fk_products_prices_product_name", - ondelete="RESTRICT", - onupdate="CASCADE", + ondelete=RefActions.RESTRICT, + onupdate=RefActions.CASCADE, ), nullable=False, doc="Product name", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/products_to_templates.py b/packages/postgres-database/src/simcore_postgres_database/models/products_to_templates.py index 4236d95864e..44115660735 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/products_to_templates.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/products_to_templates.py @@ -1,6 +1,7 @@ import sqlalchemy as sa from ._common import ( + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, @@ -16,8 +17,8 @@ sa.String, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_products_to_templates_product_name", ), nullable=False, @@ -28,8 +29,8 @@ sa.ForeignKey( jinja2_templates.c.name, name="fk_products_to_templates_template_name", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=True, doc="Custom jinja2 template", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/project_to_groups.py b/packages/postgres-database/src/simcore_postgres_database/models/project_to_groups.py index f27c6081f6c..162a51d4d24 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/project_to_groups.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/project_to_groups.py @@ -1,7 +1,7 @@ import sqlalchemy as sa from sqlalchemy.sql import expression -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .groups import groups from .projects import projects @@ -15,8 +15,8 @@ sa.ForeignKey( projects.c.uuid, name="fk_project_to_groups_project_uuid", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), index=True, nullable=False, @@ -28,8 +28,8 @@ sa.ForeignKey( groups.c.gid, name="fk_project_to_groups_gid_groups", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=False, doc="Group unique IDentifier", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects.py b/packages/postgres-database/src/simcore_postgres_database/models/projects.py index 778d2b80eb5..93ff3a74ea3 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects.py @@ -7,6 +7,7 @@ from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.sql import expression, func +from ._common import RefActions from .base import metadata @@ -64,8 +65,8 @@ class ProjectType(enum.Enum): sa.ForeignKey( "users.id", name="fk_projects_prj_owner_users", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), nullable=True, doc="Project's owner", @@ -161,8 +162,8 @@ class ProjectType(enum.Enum): sa.BigInteger, sa.ForeignKey( "workspaces.workspace_id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_projects_to_workspaces_id", ), nullable=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_comments.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_comments.py index fb07f56a202..919b143bff3 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_comments.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_comments.py @@ -1,6 +1,6 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .projects import projects from .users import users @@ -22,8 +22,8 @@ sa.ForeignKey( projects.c.uuid, name="fk_projects_comments_project_uuid", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), index=True, nullable=False, @@ -36,7 +36,7 @@ sa.ForeignKey( users.c.id, name="fk_projects_comments_user_id", - ondelete="SET NULL", + ondelete=RefActions.SET_NULL, ), doc="user who created the comment", nullable=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_metadata.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_metadata.py index b2d2d4f640a..c5379f407d4 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_metadata.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_metadata.py @@ -7,6 +7,7 @@ from sqlalchemy.dialects.postgresql import JSONB from ._common import ( + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, @@ -42,8 +43,8 @@ sa.String, sa.ForeignKey( projects.c.uuid, - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_projects_metadata_project_uuid", ), nullable=False, @@ -88,15 +89,15 @@ sa.ForeignKeyConstraint( ("parent_project_uuid", "parent_node_id"), (projects_nodes.c.project_uuid, projects_nodes.c.node_id), - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, name="fk_projects_metadata_parent_node_id", ), sa.ForeignKeyConstraint( ("root_parent_project_uuid", "root_parent_node_id"), (projects_nodes.c.project_uuid, projects_nodes.c.node_id), - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, name="fk_projects_metadata_root_parent_node_id", ), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_networks.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_networks.py index efac321a539..905c1aa4cf0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_networks.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_networks.py @@ -1,6 +1,7 @@ import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB +from ._common import RefActions from .base import metadata from .projects import projects @@ -13,8 +14,8 @@ sa.ForeignKey( projects.c.uuid, name="fk_projects_networks_project_uuid_projects", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), primary_key=True, doc="project reference and primary key for this table", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_node_to_pricing_unit.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_node_to_pricing_unit.py index aa0e42a35a3..903466c3f93 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_node_to_pricing_unit.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_node_to_pricing_unit.py @@ -7,6 +7,7 @@ import sqlalchemy as sa from ._common import ( + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, @@ -22,8 +23,8 @@ sa.BIGINT, sa.ForeignKey( projects_nodes.c.project_node_id, - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_projects_nodes__project_node_to_pricing_unit__uuid", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_nodes.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_nodes.py index 1da6f138cdb..f4b569270c4 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_nodes.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_nodes.py @@ -8,6 +8,7 @@ from sqlalchemy.dialects.postgresql import JSONB from ._common import ( + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, @@ -31,8 +32,8 @@ sa.String, sa.ForeignKey( projects.c.uuid, - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_projects_to_projects_nodes_to_projects_uuid", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_tags.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_tags.py index 223271872b7..3507fc8d239 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_tags.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_tags.py @@ -1,5 +1,6 @@ import sqlalchemy as sa +from ._common import RefActions from .base import metadata from .projects import projects from .tags import tags @@ -15,8 +16,8 @@ sa.BigInteger, sa.ForeignKey( projects.c.id, - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, name="project_tags_project_id_fkey", ), nullable=True, # <-- NULL means that project was deleted @@ -25,7 +26,9 @@ sa.Column( "tag_id", sa.BigInteger, - sa.ForeignKey(tags.c.id, onupdate="CASCADE", ondelete="CASCADE"), + sa.ForeignKey( + tags.c.id, onupdate=RefActions.CASCADE, ondelete=RefActions.CASCADE + ), nullable=False, ), sa.Column( diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_to_folders.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_to_folders.py index ba2b7334621..f86725a8119 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_to_folders.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_to_folders.py @@ -1,6 +1,6 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .folders_v2 import folders_v2 @@ -13,8 +13,8 @@ sa.ForeignKey( "projects.uuid", name="fk_projects_to_folders_to_projects_uuid", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), ), sa.Column( @@ -23,8 +23,8 @@ sa.ForeignKey( folders_v2.c.folder_id, name="fk_projects_to_folders_to_folders_id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), ), sa.Column( @@ -32,8 +32,8 @@ sa.BigInteger, sa.ForeignKey( "users.id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_projects_to_folders_to_user_id", ), nullable=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_to_products.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_to_products.py index e6a1d1cd530..47a430f0a00 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_to_products.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_to_products.py @@ -1,6 +1,6 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata projects_to_products = sa.Table( @@ -11,8 +11,8 @@ sa.String, sa.ForeignKey( "projects.uuid", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_projects_to_products_product_uuid", ), nullable=False, @@ -23,8 +23,8 @@ sa.String, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_projects_to_products_product_name", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_to_wallet.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_to_wallet.py index dfe3108a845..74e7a7ef635 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_to_wallet.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_to_wallet.py @@ -1,6 +1,6 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .projects import projects from .wallets import wallets @@ -14,8 +14,8 @@ sa.ForeignKey( projects.c.uuid, name="fk_projects_comments_project_uuid", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), index=True, primary_key=True, @@ -28,8 +28,8 @@ sa.ForeignKey( wallets.c.wallet_id, name="fk_projects_wallet_wallets_id", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects_version_control.py b/packages/postgres-database/src/simcore_postgres_database/models/projects_version_control.py index c0fd0816e51..7d183f03942 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects_version_control.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects_version_control.py @@ -6,6 +6,7 @@ from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.sql import func +from ._common import RefActions from .base import metadata from .projects import projects @@ -32,8 +33,8 @@ sa.ForeignKey( projects.c.uuid, name="fk_projects_vc_repos_project_uuid", - ondelete="CASCADE", # if project is deleted, all references in project_vc_* tables are deleted except for projects_vc_snapshots. - onupdate="CASCADE", + ondelete=RefActions.CASCADE, # if project is deleted, all references in project_vc_* tables are deleted except for projects_vc_snapshots. + onupdate=RefActions.CASCADE, ), nullable=False, unique=True, @@ -113,8 +114,8 @@ sa.ForeignKey( projects_vc_repos.c.id, name="fk_projects_vc_commits_repo_id", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, doc="Repository to which this commit belongs", @@ -125,7 +126,7 @@ sa.ForeignKey( "projects_vc_commits.id", name="fk_projects_vc_commits_parent_commit_id", - onupdate="CASCADE", + onupdate=RefActions.CASCADE, ), nullable=True, doc="Preceding commit", @@ -136,8 +137,8 @@ sa.ForeignKey( projects_vc_snapshots.c.checksum, name="fk_projects_vc_commits_snapshot_checksum", - ondelete="RESTRICT", - onupdate="CASCADE", + ondelete=RefActions.RESTRICT, + onupdate=RefActions.CASCADE, ), nullable=False, doc="SHA-1 checksum of snapshot." @@ -175,8 +176,8 @@ sa.ForeignKey( projects_vc_repos.c.id, name="fk_projects_vc_tags_repo_id", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, doc="Repository to which this commit belongs", @@ -187,8 +188,8 @@ sa.ForeignKey( projects_vc_commits.c.id, name="fk_projects_vc_tags_commit_id", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, doc="Points to the tagged commit", @@ -243,8 +244,8 @@ sa.ForeignKey( projects_vc_repos.c.id, name="projects_vc_branches_repo_id", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, doc="Repository to which this branch belongs", @@ -255,8 +256,8 @@ sa.ForeignKey( projects_vc_commits.c.id, name="fk_projects_vc_branches_head_commit_id", - ondelete="RESTRICT", - onupdate="CASCADE", + ondelete=RefActions.RESTRICT, + onupdate=RefActions.CASCADE, ), nullable=True, doc="Points to the head commit of this branchNull heads are detached", @@ -299,8 +300,8 @@ sa.ForeignKey( projects_vc_repos.c.id, name="projects_vc_branches_repo_id", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), primary_key=True, nullable=False, @@ -312,8 +313,8 @@ sa.ForeignKey( projects_vc_branches.c.id, name="fk_projects_vc_heads_head_branch_id", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), unique=True, nullable=True, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py index d0a164c054d..d1501a42431 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py @@ -5,7 +5,12 @@ import sqlalchemy as sa -from ._common import NUMERIC_KWARGS, column_created_datetime, column_modified_datetime +from ._common import ( + NUMERIC_KWARGS, + RefActions, + column_created_datetime, + column_modified_datetime, +) from .base import metadata @@ -130,7 +135,7 @@ class CreditTransactionClassification(str, enum.Enum): "resource_tracker_service_runs.service_run_id", ], name="resource_tracker_credit_trans_fkey", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py index 820ec42fc50..5fd77bbbaad 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plan_to_service.py @@ -4,7 +4,7 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata resource_tracker_pricing_plan_to_service = sa.Table( @@ -16,8 +16,8 @@ sa.ForeignKey( "resource_tracker_pricing_plans.pricing_plan_id", name="fk_resource_tracker_pricing_details_pricing_plan_id", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), nullable=False, doc="Identifier index", @@ -49,7 +49,7 @@ ["service_key", "service_version"], ["services_meta_data.key", "services_meta_data.version"], name="fk_rut_pricing_plan_to_service_key_and_version", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py index 81d98ebcac1..e0e1c69efa6 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_plans.py @@ -4,7 +4,7 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata @@ -35,8 +35,8 @@ class PricingPlanClassification(str, enum.Enum): sa.String, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_rut_pricing_plans_product_name", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_unit_costs.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_unit_costs.py index 46031532387..7a4c4e5f6a1 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_unit_costs.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_unit_costs.py @@ -6,7 +6,12 @@ """ import sqlalchemy as sa -from ._common import NUMERIC_KWARGS, column_created_datetime, column_modified_datetime +from ._common import ( + NUMERIC_KWARGS, + RefActions, + column_created_datetime, + column_modified_datetime, +) from .base import metadata resource_tracker_pricing_unit_costs = sa.Table( @@ -25,8 +30,8 @@ sa.ForeignKey( "resource_tracker_pricing_plans.pricing_plan_id", name="fk_resource_tracker_pricing_units_costs_pricing_plan_id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=False, doc="Foreign key to pricing plan", @@ -44,8 +49,8 @@ sa.ForeignKey( "resource_tracker_pricing_units.pricing_unit_id", name="fk_resource_tracker_pricing_units_costs_pricing_unit_id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=False, doc="Foreign key to pricing unit", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_units.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_units.py index 6b047c2207a..aecbc1d07e1 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_units.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_pricing_units.py @@ -7,7 +7,7 @@ import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata resource_tracker_pricing_units = sa.Table( @@ -26,8 +26,8 @@ sa.ForeignKey( "resource_tracker_pricing_plans.pricing_plan_id", name="fk_resource_tracker_pricing_units_pricing_plan_id", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=False, doc="Foreign key to pricing plan", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/services.py b/packages/postgres-database/src/simcore_postgres_database/models/services.py index b329a1e8156..30fbf5af696 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/services.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/services.py @@ -2,6 +2,7 @@ from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.sql import expression +from ._common import RefActions from .base import metadata services_meta_data = sa.Table( @@ -33,8 +34,8 @@ sa.ForeignKey( "groups.gid", name="fk_services_meta_data_gid_groups", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), nullable=True, doc="Identifier of the group that owns this service (editable)", @@ -141,8 +142,8 @@ sa.ForeignKey( "groups.gid", name="fk_services_gid_groups", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Group Identifier of user that get these access-rights", ), @@ -168,8 +169,8 @@ sa.ForeignKey( "products.name", name="fk_services_name_products", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Product Identifier", ), @@ -192,8 +193,8 @@ sa.ForeignKeyConstraint( ["key", "version"], ["services_meta_data.key", "services_meta_data.version"], - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), sa.PrimaryKeyConstraint( "key", "version", "gid", "product_name", name="services_access_pk" diff --git a/packages/postgres-database/src/simcore_postgres_database/models/services_compatibility.py b/packages/postgres-database/src/simcore_postgres_database/models/services_compatibility.py index aa3929385e3..d151b665885 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/services_compatibility.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/services_compatibility.py @@ -11,6 +11,7 @@ from typing_extensions import NotRequired, Required from ._common import ( + RefActions, column_created_datetime, column_modified_by_user, column_modified_datetime, @@ -61,8 +62,8 @@ class CompatiblePolicyDict(typing_extensions.TypedDict, total=False): sa.ForeignKeyConstraint( ["key", "version"], ["services_meta_data.key", "services_meta_data.version"], - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), sa.PrimaryKeyConstraint("key", "version", name="services_compatibility_pk"), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/services_consume_filetypes.py b/packages/postgres-database/src/simcore_postgres_database/models/services_consume_filetypes.py index e46df8670e8..65c6c8546b3 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/services_consume_filetypes.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/services_consume_filetypes.py @@ -7,6 +7,7 @@ """ import sqlalchemy as sa +from ._common import RefActions from .base import metadata # @@ -75,8 +76,8 @@ sa.ForeignKeyConstraint( ["service_key", "service_version"], ["services_meta_data.key", "services_meta_data.version"], - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), # This table stores services (key:version) that consume filetype by AT LEAST one input_port # if more ports can consume, then it should only be added once in this table diff --git a/packages/postgres-database/src/simcore_postgres_database/models/services_environments.py b/packages/postgres-database/src/simcore_postgres_database/models/services_environments.py index 3248191e7a1..498191b7267 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/services_environments.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/services_environments.py @@ -3,7 +3,7 @@ import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata # Intentionally includes the term "SECRET" to avoid leaking this value on a public domain @@ -36,8 +36,8 @@ sa.ForeignKey( "products.name", name="fk_services_name_products", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), # NOTE: since this is part of the primary key this is required # NOTE: an alternative would be to not use this as a primary key @@ -59,8 +59,8 @@ sa.ForeignKeyConstraint( ["service_key", "service_base_version"], ["services_meta_data.key", "services_meta_data.version"], - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, # NOTE: this might be a problem: if a version in the metadata is deleted, # all versions above will take the secret_map for the previous one. ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/services_specifications.py b/packages/postgres-database/src/simcore_postgres_database/models/services_specifications.py index a8b16b06f91..452be5e25f0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/services_specifications.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/services_specifications.py @@ -8,6 +8,7 @@ import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB +from ._common import RefActions from .base import metadata services_specifications = sa.Table( @@ -31,8 +32,8 @@ sa.ForeignKey( "groups.gid", name="fk_services_specifications_gid_groups", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Group Identifier", ), @@ -52,8 +53,8 @@ sa.ForeignKeyConstraint( ["service_key", "service_version"], ["services_meta_data.key", "services_meta_data.version"], - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), # This table stores services (key:version) that consume filetype by AT LEAST one input_port # if more ports can consume, then it should only be added once in this table diff --git a/packages/postgres-database/src/simcore_postgres_database/models/services_tags.py b/packages/postgres-database/src/simcore_postgres_database/models/services_tags.py index c774cdcd317..083ea9f2807 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/services_tags.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/services_tags.py @@ -1,5 +1,6 @@ import sqlalchemy as sa +from ._common import RefActions from .base import metadata from .tags import tags @@ -26,7 +27,9 @@ sa.Column( "tag_id", sa.BigInteger, - sa.ForeignKey(tags.c.id, onupdate="CASCADE", ondelete="CASCADE"), + sa.ForeignKey( + tags.c.id, onupdate=RefActions.CASCADE, ondelete=RefActions.CASCADE + ), nullable=False, doc="Identifier of the tag assigned to this specific service (service_key, service_version).", ), @@ -34,8 +37,8 @@ sa.ForeignKeyConstraint( ["service_key", "service_version"], ["services_meta_data.key", "services_meta_data.version"], - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), sa.UniqueConstraint( "service_key", "service_version", "tag_id", name="services_tags_uc" diff --git a/packages/postgres-database/src/simcore_postgres_database/models/tags_access_rights.py b/packages/postgres-database/src/simcore_postgres_database/models/tags_access_rights.py index 9078a9254f1..b818c975817 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/tags_access_rights.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/tags_access_rights.py @@ -1,6 +1,6 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .groups import groups from .tags import tags @@ -17,8 +17,8 @@ sa.BigInteger(), sa.ForeignKey( tags.c.id, - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_tag_to_group_tag_id", ), nullable=False, @@ -29,8 +29,8 @@ sa.BigInteger, sa.ForeignKey( groups.c.gid, - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_tag_to_group_group_id", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/user_preferences.py b/packages/postgres-database/src/simcore_postgres_database/models/user_preferences.py index 8d3cdde98d5..e380cd23b94 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/user_preferences.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/user_preferences.py @@ -1,5 +1,6 @@ import sqlalchemy as sa +from ._common import RefActions from .base import metadata from .products import products from .users import users @@ -12,8 +13,8 @@ def _user_id_column(fk_name: str) -> sa.Column: sa.ForeignKey( users.c.id, name=fk_name, - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, ) @@ -26,8 +27,8 @@ def _product_name_column(fk_name: str) -> sa.Column: sa.ForeignKey( products.c.name, name=fk_name, - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, ) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/user_to_projects.py b/packages/postgres-database/src/simcore_postgres_database/models/user_to_projects.py index 45147bef610..4a66e0be611 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/user_to_projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/user_to_projects.py @@ -1,5 +1,6 @@ import sqlalchemy as sa +from ._common import RefActions from .base import metadata from .projects import projects from .users import users @@ -15,8 +16,8 @@ sa.ForeignKey( users.c.id, name="fk_user_to_projects_id_users", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, ), @@ -26,8 +27,8 @@ sa.ForeignKey( projects.c.id, name="fk_user_to_projects_id_projects", - ondelete="CASCADE", - onupdate="CASCADE", + ondelete=RefActions.CASCADE, + onupdate=RefActions.CASCADE, ), nullable=False, ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/users.py b/packages/postgres-database/src/simcore_postgres_database/models/users.py index 90d5e063662..61b8c321130 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/users.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/users.py @@ -3,6 +3,7 @@ import sqlalchemy as sa +from ._common import RefActions from .base import metadata _USER_ROLE_TO_LEVEL = { @@ -113,8 +114,8 @@ class UserStatus(str, Enum): sa.ForeignKey( "groups.gid", name="fk_users_gid_groups", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), doc="User's group ID", ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/users_details.py b/packages/postgres-database/src/simcore_postgres_database/models/users_details.py index 2e7a7ab79c5..555e623dbdc 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/users_details.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/users_details.py @@ -2,6 +2,7 @@ from sqlalchemy.dialects import postgresql from ._common import ( + RefActions, column_created_datetime, column_modified_datetime, register_modified_datetime_auto_update_trigger, @@ -22,8 +23,8 @@ sa.Integer, sa.ForeignKey( users.c.id, - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), nullable=True, doc="None if row was added during pre-registration or join column with `users` after registration", @@ -71,8 +72,8 @@ sa.Integer, sa.ForeignKey( users.c.id, - onupdate="CASCADE", - ondelete="SET NULL", + onupdate=RefActions.CASCADE, + ondelete=RefActions.SET_NULL, ), nullable=True, doc="PO user that issued this pre-registration", diff --git a/packages/postgres-database/src/simcore_postgres_database/models/wallet_to_groups.py b/packages/postgres-database/src/simcore_postgres_database/models/wallet_to_groups.py index a5d4016fa51..7679b5f5285 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/wallet_to_groups.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/wallet_to_groups.py @@ -1,7 +1,7 @@ import sqlalchemy as sa from sqlalchemy.sql import expression -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .groups import groups from .wallets import wallets @@ -15,8 +15,8 @@ sa.ForeignKey( wallets.c.wallet_id, name="fk_wallet_to_groups_id_wallets", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Wallet unique ID", ), @@ -26,8 +26,8 @@ sa.ForeignKey( groups.c.gid, name="fk_wallet_to_groups_gid_groups", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Group unique IDentifier", ), diff --git a/packages/postgres-database/src/simcore_postgres_database/models/wallets.py b/packages/postgres-database/src/simcore_postgres_database/models/wallets.py index 3c765529976..27fa821a6bf 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/wallets.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/wallets.py @@ -2,7 +2,7 @@ import sqlalchemy as sa -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata @@ -30,8 +30,8 @@ class WalletStatus(str, enum.Enum): sa.ForeignKey( "groups.gid", name="fk_wallets_gid_groups", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), nullable=False, doc="Identifier of the group that owns this wallet (Should be just PRIMARY GROUP)", @@ -55,8 +55,8 @@ class WalletStatus(str, enum.Enum): sa.String, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_wallets_product_name", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py b/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py index 7f1b7963929..756bbe9642e 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/workspaces.py @@ -1,6 +1,7 @@ import sqlalchemy as sa from ._common import ( + RefActions, column_created_datetime, column_modified_datetime, column_trashed_by_user, @@ -34,8 +35,8 @@ sa.ForeignKey( "groups.gid", name="fk_workspaces_gid_groups", - onupdate="CASCADE", - ondelete="RESTRICT", + onupdate=RefActions.CASCADE, + ondelete=RefActions.RESTRICT, ), nullable=False, doc="Identifier of the group that owns this workspace (Should be just PRIMARY GROUP)", @@ -45,8 +46,8 @@ sa.String, sa.ForeignKey( "products.name", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, name="fk_workspaces_product_name", ), nullable=False, diff --git a/packages/postgres-database/src/simcore_postgres_database/models/workspaces_access_rights.py b/packages/postgres-database/src/simcore_postgres_database/models/workspaces_access_rights.py index 960ef643538..2a247fb477f 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/workspaces_access_rights.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/workspaces_access_rights.py @@ -1,7 +1,7 @@ import sqlalchemy as sa from sqlalchemy.sql import expression -from ._common import column_created_datetime, column_modified_datetime +from ._common import RefActions, column_created_datetime, column_modified_datetime from .base import metadata from .groups import groups from .workspaces import workspaces @@ -15,8 +15,8 @@ sa.ForeignKey( workspaces.c.workspace_id, name="fk_workspaces_access_rights_id_workspaces", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Workspace unique ID", ), @@ -26,8 +26,8 @@ sa.ForeignKey( groups.c.gid, name="fk_workspaces_access_rights_gid_groups", - onupdate="CASCADE", - ondelete="CASCADE", + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, ), doc="Group unique IDentifier", ), diff --git a/packages/postgres-database/tests/test_models_tags.py b/packages/postgres-database/tests/test_models_tags.py index 71a7ba4702d..19d654b31bf 100644 --- a/packages/postgres-database/tests/test_models_tags.py +++ b/packages/postgres-database/tests/test_models_tags.py @@ -23,7 +23,9 @@ def test_migration_downgrade_script(): sa.Column( "user_id", sa.BigInteger, - sa.ForeignKey("users.id", onupdate="CASCADE", ondelete="CASCADE"), + sa.ForeignKey( + "users.id", onupdate=RefActions.CASCADE, ondelete=RefActions.CASCADE + ), nullable=False, ), sa.Column("name", sa.String, nullable=False), From 966980a84225e05b580d437083b804390d3b39af Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:20:31 +0100 Subject: [PATCH 51/52] minor --- packages/postgres-database/tests/test_models_tags.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/postgres-database/tests/test_models_tags.py b/packages/postgres-database/tests/test_models_tags.py index 19d654b31bf..7b129e5edc7 100644 --- a/packages/postgres-database/tests/test_models_tags.py +++ b/packages/postgres-database/tests/test_models_tags.py @@ -6,6 +6,7 @@ import pytest import sqlalchemy as sa +from simcore_postgres_database.models._common import RefActions from simcore_postgres_database.models.base import metadata from simcore_postgres_database.models.tags_access_rights import tags_access_rights from simcore_postgres_database.models.users import users From b373a4bcf83781b77b1074ce288f178016f1c717 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sat, 23 Nov 2024 12:29:20 +0100 Subject: [PATCH 52/52] cleanup --- .../api/v0/openapi.yaml | 339 +++++++++--------- 1 file changed, 176 insertions(+), 163 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 cb458e71e2f..688d2187cb3 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 @@ -2605,7 +2605,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateFolderBodyParams' + $ref: '#/components/schemas/FolderCreateBodyParams' responses: '201': description: Successful Response @@ -2775,7 +2775,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PutFolderBodyParams' + $ref: '#/components/schemas/FolderReplaceBodyParams' responses: '200': description: Successful Response @@ -5535,21 +5535,21 @@ paths: summary: Trash Workspace operationId: trash_workspace parameters: - - required: true + - name: workspace_id + in: path + required: true schema: - title: Workspace Id - exclusiveMinimum: true type: integer + exclusiveMinimum: true + title: Workspace Id minimum: 0 - name: workspace_id - in: path - - required: false + - name: force + in: query + required: false schema: - title: Force type: boolean default: false - name: force - in: query + title: Force responses: '204': description: Successful Response @@ -5568,14 +5568,14 @@ paths: summary: Untrash Workspace operationId: untrash_workspace parameters: - - required: true + - name: workspace_id + in: path + required: true schema: - title: Workspace Id - exclusiveMinimum: true type: integer + exclusiveMinimum: true + title: Workspace Id minimum: 0 - name: workspace_id - in: path responses: '204': description: Successful Response @@ -5803,47 +5803,63 @@ paths: schema: $ref: '#/components/schemas/Envelope_CheckpointApiModel_' /v0/workspaces: + post: + tags: + - workspaces + summary: Create Workspace + operationId: create_workspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceCreateBodyParams' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_WorkspaceGet_' get: tags: - workspaces summary: List Workspaces operationId: list_workspaces parameters: - - required: false + - name: order_by + in: query + required: false schema: - title: Order By type: string - description: Order by field (`modified_at|name`) and direction (`asc|desc`). - The default sorting order is `{"field":"modified_at","direction":"desc"}`. + contentMediaType: application/json + contentSchema: {} default: '{"field":"modified","direction":"desc"}' - example: '{"field":"name","direction":"desc"}' - name: order_by + title: Order By + - name: filters in: query - - required: false + required: false schema: + anyOf: + - type: string + contentMediaType: application/json + contentSchema: {} + - type: 'null' title: Filters - type: string - description: Custom filter query parameter encoded as JSON - name: filters + - name: limit in: query - - required: false + required: false schema: - title: Limit - exclusiveMaximum: true - minimum: 1 type: integer default: 20 - maximum: 50 - name: limit + title: Limit + - name: offset in: query - - required: false + required: false schema: - title: Offset - minimum: 0 type: integer default: 0 - name: offset - in: query + title: Offset responses: '200': description: Successful Response @@ -5851,24 +5867,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Envelope_list_WorkspaceGet__' - post: - tags: - - workspaces - summary: Create Workspace - operationId: create_workspace - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/WorkspaceCreateBodyParams' - required: true - responses: - '201': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/Envelope_WorkspaceGet_' /v0/workspaces/{workspace_id}: get: tags: @@ -5910,7 +5908,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PutWorkspaceBodyParams' + $ref: '#/components/schemas/WorkspaceReplaceBodyParams' responses: '200': description: Successful Response @@ -5964,7 +5962,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/_WorkspacesGroupsBodyParams' + $ref: '#/components/schemas/WorkspacesGroupsBodyParams' responses: '201': description: Successful Response @@ -6000,7 +5998,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/_WorkspacesGroupsBodyParams' + $ref: '#/components/schemas/WorkspacesGroupsBodyParams' responses: '200': description: Successful Response @@ -7201,32 +7199,6 @@ components: - name - alpha2 title: CountryInfoDict - CreateFolderBodyParams: - properties: - name: - type: string - maxLength: 100 - minLength: 1 - title: Name - parentFolderId: - anyOf: - - type: integer - exclusiveMinimum: true - minimum: 0 - - type: 'null' - title: Parentfolderid - workspaceId: - anyOf: - - type: integer - exclusiveMinimum: true - minimum: 0 - - type: 'null' - title: Workspaceid - additionalProperties: false - type: object - required: - - name - title: CreateFolderBodyParams CreatePricingPlanBodyParams: properties: displayName: @@ -7316,26 +7288,6 @@ components: required: - priceDollars title: CreateWalletPayment - CreateWorkspaceBodyParams: - properties: - name: - type: string - title: Name - description: - anyOf: - - type: string - - type: 'null' - title: Description - thumbnail: - anyOf: - - type: string - - type: 'null' - title: Thumbnail - additionalProperties: false - type: object - required: - - name - title: CreateWorkspaceBodyParams DatCoreFileLink: properties: store: @@ -9288,6 +9240,32 @@ components: - urls - links title: FileUploadSchema + FolderCreateBodyParams: + properties: + name: + type: string + maxLength: 100 + minLength: 1 + title: Name + parentFolderId: + anyOf: + - type: integer + exclusiveMinimum: true + minimum: 0 + - type: 'null' + title: Parentfolderid + workspaceId: + anyOf: + - type: integer + exclusiveMinimum: true + minimum: 0 + - type: 'null' + title: Workspaceid + additionalProperties: false + type: object + required: + - name + title: FolderCreateBodyParams FolderGet: properties: folderId: @@ -9344,6 +9322,25 @@ components: - workspaceId - myAccessRights title: FolderGet + FolderReplaceBodyParams: + properties: + name: + type: string + maxLength: 100 + minLength: 1 + title: Name + parentFolderId: + anyOf: + - type: integer + exclusiveMinimum: true + minimum: 0 + - type: 'null' + title: Parentfolderid + additionalProperties: false + type: object + required: + - name + title: FolderReplaceBodyParams GenerateInvitation: properties: guest: @@ -12140,25 +12137,6 @@ components: - created - modified title: ProjectsCommentsAPI - PutFolderBodyParams: - properties: - name: - type: string - maxLength: 100 - minLength: 1 - title: Name - parentFolderId: - anyOf: - - type: integer - exclusiveMinimum: true - minimum: 0 - - type: 'null' - title: Parentfolderid - additionalProperties: false - type: object - required: - - name - title: PutFolderBodyParams PutWalletBodyParams: properties: name: @@ -12183,28 +12161,6 @@ components: - thumbnail - status title: PutWalletBodyParams - PutWorkspaceBodyParams: - properties: - name: - type: string - maxLength: 100 - minLength: 1 - title: Name - description: - anyOf: - - type: string - - type: 'null' - title: Description - thumbnail: - anyOf: - - type: string - - type: 'null' - title: Thumbnail - additionalProperties: false - type: object - required: - - name - title: PutWorkspaceBodyParams RegisterBody: properties: email: @@ -14375,6 +14331,26 @@ components: - num_fds - task_counts title: WorkerMetrics + WorkspaceCreateBodyParams: + properties: + name: + type: string + title: Name + description: + anyOf: + - type: string + - type: 'null' + title: Description + thumbnail: + anyOf: + - type: string + - type: 'null' + title: Thumbnail + additionalProperties: false + type: object + required: + - name + title: WorkspaceCreateBodyParams WorkspaceGet: properties: workspaceId: @@ -14403,6 +14379,19 @@ components: type: string format: date-time title: Modifiedat + trashedAt: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Trashedat + trashedBy: + anyOf: + - type: integer + exclusiveMinimum: true + minimum: 0 + - type: 'null' + title: Trashedby myAccessRights: $ref: '#/components/schemas/AccessRights' accessRights: @@ -14418,6 +14407,8 @@ components: - thumbnail - createdAt - modifiedAt + - trashedAt + - trashedBy - myAccessRights - accessRights title: WorkspaceGet @@ -14454,6 +14445,46 @@ components: - created - modified title: WorkspaceGroupGet + WorkspaceReplaceBodyParams: + properties: + name: + type: string + maxLength: 100 + minLength: 1 + title: Name + description: + anyOf: + - type: string + - type: 'null' + title: Description + thumbnail: + anyOf: + - type: string + - type: 'null' + title: Thumbnail + additionalProperties: false + type: object + required: + - name + title: WorkspaceReplaceBodyParams + WorkspacesGroupsBodyParams: + properties: + read: + type: boolean + title: Read + write: + type: boolean + title: Write + delete: + type: boolean + title: Delete + additionalProperties: false + type: object + required: + - read + - write + - delete + title: WorkspacesGroupsBodyParams _ComputationStarted: properties: pipeline_id: @@ -14570,21 +14601,3 @@ components: - write - delete title: _WalletsGroupsBodyParams - _WorkspacesGroupsBodyParams: - properties: - read: - type: boolean - title: Read - write: - type: boolean - title: Write - delete: - type: boolean - title: Delete - additionalProperties: false - type: object - required: - - read - - write - - delete - title: _WorkspacesGroupsBodyParams