diff --git a/packages/models-library/src/models_library/api_schemas_catalog/services.py b/packages/models-library/src/models_library/api_schemas_catalog/services.py index 1038d803ff7..7accfc20a66 100644 --- a/packages/models-library/src/models_library/api_schemas_catalog/services.py +++ b/packages/models-library/src/models_library/api_schemas_catalog/services.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Any, ClassVar, TypeAlias from models_library.rpc_pagination import PageRpc @@ -5,7 +6,7 @@ from ..boot_options import BootOptions from ..emails import LowerCaseEmailStr -from ..services_access import ServiceAccessRights +from ..services_access import ServiceAccessRights, ServiceGroupAccessRightsV2 from ..services_authoring import Author from ..services_enums import ServiceType from ..services_history import ServiceRelease @@ -108,6 +109,14 @@ class Config: "owner": "redpandas@wonderland.com", } +_EXAMPLE_FILEPICKER_V2 = { + **_EXAMPLE_FILEPICKER, + "accessRights": { + "1": {"execute": True, "write": False}, + "4": {"execute": True, "write": True}, + }, +} + _EXAMPLE_SLEEPER: dict[str, Any] = { "name": "sleeper", @@ -115,7 +124,7 @@ class Config: "description": "A service which awaits for time to pass, two times.", "classifiers": [], "quality": {}, - "accessRights": {"1": {"execute_access": True, "write_access": False}}, + "accessRights": {"1": {"execute": True, "write": False}}, "key": "simcore/services/comp/itis/sleeper", "version": "2.2.1", "version_display": "2 Xtreme", @@ -204,15 +213,6 @@ class Config: } -class ServiceGroupAccessRightsV2(BaseModel): - execute: bool = False - write: bool = False - - class Config: - alias_generator = snake_to_camel - allow_population_by_field_name = True - - class ServiceGetV2(BaseModel): key: ServiceKey version: ServiceVersion @@ -291,10 +291,10 @@ class Config: ], }, { - **_EXAMPLE_FILEPICKER, + **_EXAMPLE_FILEPICKER_V2, "history": [ { - "version": _EXAMPLE_FILEPICKER["version"], + "version": _EXAMPLE_FILEPICKER_V2["version"], "version_display": "Odei Release", "released": "2025-03-25T00:00:00", } @@ -310,3 +310,28 @@ class Config: ] ServiceResourcesGet: TypeAlias = ServiceResourcesDict + + +class ServiceUpdateV2(BaseModel): + name: str | None = None + thumbnail: HttpUrl | None = None + + description: str | None = None + version_display: str | None = None + + deprecated: datetime | None = None + + classifiers: list[str] | None = None + quality: dict[str, Any] = {} + + access_rights: dict[GroupID, ServiceGroupAccessRightsV2] | None = None + + class Config: + extra = Extra.forbid + alias_generator = snake_to_camel + allow_population_by_field_name = True + + +assert set(ServiceUpdateV2.__fields__.keys()) - set( # nosec + ServiceGetV2.__fields__.keys() +) == {"deprecated"} diff --git a/packages/models-library/src/models_library/api_schemas_webserver/catalog.py b/packages/models-library/src/models_library/api_schemas_webserver/catalog.py index fba03a6a747..172575a8f92 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/catalog.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/catalog.py @@ -263,6 +263,6 @@ class Config(OutputSchema.Config): } -class CatalogServiceUpdate(api_schemas_catalog_services.ServiceUpdate): +class CatalogServiceUpdate(api_schemas_catalog_services.ServiceUpdateV2): class Config(InputSchema.Config): ... diff --git a/packages/models-library/src/models_library/services_access.py b/packages/models-library/src/models_library/services_access.py index 71b44f41350..8bc6786c695 100644 --- a/packages/models-library/src/models_library/services_access.py +++ b/packages/models-library/src/models_library/services_access.py @@ -2,10 +2,10 @@ """ - -from pydantic import BaseModel, Field +from pydantic import BaseModel, Extra, Field from .users import GroupID +from .utils.change_case import snake_to_camel class ServiceGroupAccessRights(BaseModel): @@ -18,6 +18,16 @@ class ServiceGroupAccessRights(BaseModel): ) +class ServiceGroupAccessRightsV2(BaseModel): + execute: bool = False + write: bool = False + + class Config: + alias_generator = snake_to_camel + allow_population_by_field_name = True + extra = Extra.forbid + + class ServiceAccessRights(BaseModel): access_rights: dict[GroupID, ServiceGroupAccessRights] | None = Field( None, diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py index b47a7931235..2edf919a524 100644 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py @@ -6,7 +6,7 @@ from typing import Any, cast from models_library.api_schemas_catalog import CATALOG_RPC_NAMESPACE -from models_library.api_schemas_catalog.services import ServiceGetV2, ServiceUpdate +from models_library.api_schemas_catalog.services import ServiceGetV2, ServiceUpdateV2 from models_library.products import ProductName from models_library.rabbitmq_basic_types import RPCMethodName from models_library.rpc_pagination import ( @@ -115,7 +115,7 @@ async def update_service( user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion, - update: ServiceUpdate, + update: ServiceUpdateV2, ) -> ServiceGetV2: """Updates editable fields of a service @@ -131,7 +131,7 @@ async def _call( user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion, - update: ServiceUpdate, + update: ServiceUpdateV2, ): return await rpc_client.request( CATALOG_RPC_NAMESPACE, diff --git a/services/catalog/openapi.json b/services/catalog/openapi.json index 9175ef25112..33d1ee918cb 100644 --- a/services/catalog/openapi.json +++ b/services/catalog/openapi.json @@ -2079,7 +2079,7 @@ "type": "string", "format": "date-time", "title": "Deprecated", - "description": "Owner can set the date to retire the service. Three possibilities:If None, the service is marked as `published`;If now=deprecated, the service is retired" + "description": "Owner can set the date to retire the service. Three possibilities:If None, the service is marked as `published`;If now=deprecated, the service is retired" }, "classifiers": { "items": { @@ -2558,7 +2558,7 @@ "type": "string", "format": "date-time", "title": "Deprecated", - "description": "Owner can set the date to retire the service. Three possibilities:If None, the service is marked as `published`;If now=deprecated, the service is retired" + "description": "Owner can set the date to retire the service. Three possibilities:If None, the service is marked as `published`;If now=deprecated, the service is retired" }, "classifiers": { "items": { diff --git a/services/catalog/src/simcore_service_catalog/api/rpc/_services.py b/services/catalog/src/simcore_service_catalog/api/rpc/_services.py index e44a6d01140..cb102301ecc 100644 --- a/services/catalog/src/simcore_service_catalog/api/rpc/_services.py +++ b/services/catalog/src/simcore_service_catalog/api/rpc/_services.py @@ -6,7 +6,7 @@ from models_library.api_schemas_catalog.services import ( PageRpcServicesGetV2, ServiceGetV2, - ServiceUpdate, + ServiceUpdateV2, ) from models_library.products import ProductName from models_library.rpc_pagination import DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, PageLimitInt @@ -124,7 +124,7 @@ async def update_service( user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion, - update: ServiceUpdate, + update: ServiceUpdateV2, ) -> ServiceGetV2: """Updates editable fields of a service""" diff --git a/services/catalog/src/simcore_service_catalog/services/services_api.py b/services/catalog/src/simcore_service_catalog/services/services_api.py index 792fa03f912..79df4f1a3dd 100644 --- a/services/catalog/src/simcore_service_catalog/services/services_api.py +++ b/services/catalog/src/simcore_service_catalog/services/services_api.py @@ -1,13 +1,10 @@ import logging -from models_library.api_schemas_catalog.services import ( - ServiceGetV2, - ServiceGroupAccessRightsV2, - ServiceUpdate, -) +from models_library.api_schemas_catalog.services import ServiceGetV2, ServiceUpdateV2 from models_library.emails import LowerCaseEmailStr from models_library.products import ProductName from models_library.rest_pagination import PageLimitInt +from models_library.services_access import ServiceGroupAccessRightsV2 from models_library.services_enums import ServiceType from models_library.services_history import Compatibility, ServiceRelease from models_library.services_metadata_published import ServiceMetaDataPublished @@ -219,7 +216,7 @@ async def update_service( user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion, - update: ServiceUpdate, + update: ServiceUpdateV2, ) -> ServiceGetV2: if is_function_service(service_key): @@ -273,8 +270,8 @@ async def update_service( key=service_key, version=service_version, gid=gid, - execute_access=rights.execute_access, - write_access=rights.write_access, + execute_access=rights.execute, + write_access=rights.write, product_name=product_name, ) for gid, rights in update.access_rights.items() diff --git a/services/web/server/VERSION b/services/web/server/VERSION index d3568f30dc8..72a8a6313bb 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.40.5 +0.41.0 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 5313c586d29..802ba3451b1 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.40.4 +current_version = 0.41.0 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False @@ -12,13 +12,13 @@ commit_args = --no-verify [tool:pytest] addopts = --strict-markers asyncio_mode = auto -markers = +markers = slow: marks tests as slow (deselect with '-m "not slow"') acceptance_test: "marks tests as 'acceptance tests' i.e. does the system do what the user expects? Typically those are workflows." testit: "marks test to run during development" heavy_load: "mark tests that require large amount of data" [mypy] -plugins = +plugins = pydantic.mypy sqlalchemy.ext.mypy.plugin 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 7204cd82d4b..6ab5bc5c659 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.40.5 + version: 0.41.0 servers: - url: '' description: webserver @@ -6070,12 +6070,6 @@ components: title: CatalogServiceUpdate type: object properties: - accessRights: - title: Accessrights - type: object - additionalProperties: - $ref: '#/components/schemas/ServiceGroupAccessRights' - description: service access rights per group id name: title: Name type: string @@ -6094,9 +6088,6 @@ components: deprecated: title: Deprecated type: string - description: Owner can set the date to retire the service. Three possibilities:If - None, the service is marked as `published`;If now=deprecated, the service is retired format: date-time classifiers: title: Classifiers @@ -6107,63 +6098,11 @@ components: title: Quality type: object default: {} - example: accessRights: - 1: - execute_access: false - write_access: false - 2: - execute_access: true - write_access: true - 44: - execute_access: false - write_access: false - name: My Human Readable Service Name - description: An interesting service that does something - classifiers: - - RRID:SCR_018997 - - RRID:SCR_019001 - quality: - tsr: - r01: - level: 3 - references: '' - r02: - level: 2 - references: '' - r03: - level: 0 - references: '' - r04: - level: 0 - references: '' - r05: - level: 2 - references: '' - r06: - level: 0 - references: '' - r07: - level: 0 - references: '' - r08: - level: 1 - references: '' - r09: - level: 0 - references: '' - r10: - level: 0 - references: '' - enabled: true - annotations: - vandv: '' - purpose: '' - standards: '' - limitations: '' - documentation: '' - certificationLink: '' - certificationStatus: Uncertified + title: Accessrights + type: object + additionalProperties: + $ref: '#/components/schemas/ServiceGroupAccessRightsV2' ChangePasswordBody: title: ChangePasswordBody required: diff --git a/services/web/server/src/simcore_service_webserver/catalog/_api.py b/services/web/server/src/simcore_service_webserver/catalog/_api.py index b5bffe5a764..9441af6e30e 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_api.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_api.py @@ -4,7 +4,7 @@ from aiohttp import web from aiohttp.web import Request -from models_library.api_schemas_catalog.services import ServiceUpdate +from models_library.api_schemas_catalog.services import ServiceUpdateV2 from models_library.api_schemas_webserver.catalog import ( ServiceInputGet, ServiceInputKey, @@ -156,7 +156,7 @@ async def dev_update_service( user_id=user_id, service_key=service_key, service_version=service_version, - update=ServiceUpdate.parse_obj(update_data), + update=ServiceUpdateV2.parse_obj(update_data), ) data = jsonable_encoder(service, exclude_unset=True) diff --git a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py index 1526b7d25a3..e6fee8b174e 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py +++ b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py @@ -194,6 +194,7 @@ async def test_dev_get_and_patch_service( description="bar", classifiers=None, versionDisplay="Some nice name", + accessRights={1: {"execute": True, "write": True}}, ) response = await client.patch( f"{url}", json=jsonable_encoder(update, exclude_unset=True) @@ -205,9 +206,10 @@ async def test_dev_get_and_patch_service( model = parse_obj_as(CatalogServiceGet, data) assert model.key == service_key assert model.version == service_version - assert model.name == "foo" - assert model.description == "bar" - assert model.version_display == "Some nice name" + assert model.name == update.name + assert model.description == update.description + assert model.version_display == update.version_display + assert model.access_rights == update.access_rights assert mocked_rpc_catalog_service_api["get_service"].call_count == 1 assert mocked_rpc_catalog_service_api["update_service"].call_count == 1