From d8f33ba4971b033a323fff9f6befc2a2e7f1287c Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:49:03 +0200 Subject: [PATCH 01/11] initial call --- .../projects/_projects_service.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index c3490abbe84..c5ae0eeee21 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -1366,6 +1366,12 @@ async def is_node_id_present_in_any_project_workbench( return await db_legacy.node_id_exists(node_id) +async def _is_service_collaborative( + app: web.Application, *, key: ServiceKey, version: ServiceVersion +) -> bool: + return False + + async def _get_node_share_state( app: web.Application, *, user_id: UserID, project_uuid: ProjectID, node_id: NodeID ) -> NodeShareState: @@ -1374,15 +1380,19 @@ async def _get_node_share_state( ) if _is_node_dynamic(node.key): - # if the service is dynamic and running it is locked + # if the service is dynamic and running it is locked if it is not collaborative service = await dynamic_scheduler_service.get_dynamic_service( app, node_id=node_id ) if isinstance(service, DynamicServiceGet | NodeGet): # service is running + collaborative_service = await _is_service_collaborative( + app, key=node.key, version=node.version + ) + return NodeShareState( - locked=True, + locked=not collaborative_service, current_user_groupids=[ await users_service.get_user_primary_group_id( app, TypeAdapter(UserID).validate_python(service.user_id) From 77c4fbe5dae60b2c3ae000e7b8cc8e34deff80bd Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:56:21 +0200 Subject: [PATCH 02/11] connected --- .../projects/_projects_service.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index c5ae0eeee21..1de25ef85a7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -1367,13 +1367,27 @@ async def is_node_id_present_in_any_project_workbench( async def _is_service_collaborative( - app: web.Application, *, key: ServiceKey, version: ServiceVersion + app: web.Application, + *, + user_id: UserID, + key: ServiceKey, + version: ServiceVersion, + product_name: ProductName, ) -> bool: - return False + service = await catalog_service.get_service( + app, user_id=user_id, key=key, version=version, product_name=product_name + ) + + return service.get("collaborative", False) is True async def _get_node_share_state( - app: web.Application, *, user_id: UserID, project_uuid: ProjectID, node_id: NodeID + app: web.Application, + *, + user_id: UserID, + project_uuid: ProjectID, + node_id: NodeID, + product_name: ProductName, ) -> NodeShareState: node = await _projects_nodes_repository.get( app, project_id=project_uuid, node_id=node_id @@ -1388,7 +1402,11 @@ async def _get_node_share_state( if isinstance(service, DynamicServiceGet | NodeGet): # service is running collaborative_service = await _is_service_collaborative( - app, key=node.key, version=node.version + app, + key=node.key, + version=node.version, + user_id=user_id, + product_name=product_name, ) return NodeShareState( From 35250b3f6d0df68cefe8eb7efb970a7806e1ca41 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:13:41 +0200 Subject: [PATCH 03/11] added product_name --- .../projects/_controller/projects_states_rest.py | 1 + .../projects/_crud_api_create.py | 1 + .../projects/_crud_api_read.py | 10 +++++----- .../projects/_projects_service.py | 7 +++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py index 024b42f7f66..2f0bac3310d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py @@ -165,6 +165,7 @@ async def open_project(request: web.Request) -> web.Response: user_id=req_ctx.user_id, project=project, app=request.app, + product_name=req_ctx.product_name, ) await _projects_service.notify_project_state_update(request.app, project) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py index cff404046c9..72afac900af 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py @@ -433,6 +433,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche user_id=user_id, project=new_project, app=request.app, + product_name=product_name, ) await progress.update() diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index 6bc5e29f768..abaf345b48b 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -10,6 +10,7 @@ from aiohttp import web from models_library.folders import FolderID, FolderQuery, FolderScope +from models_library.products import ProductName from models_library.projects import ProjectTemplateType from models_library.rest_ordering import OrderBy from models_library.users import UserID @@ -51,6 +52,7 @@ async def _aggregate_data_to_projects_from_other_sources( *, db_projects: list[ProjectDict], user_id: UserID, + product_name: ProductName, ) -> list[ProjectDict]: """ Aggregates data to each project from other sources, first as a batch-update and then as a parallel-update. @@ -73,9 +75,7 @@ async def _aggregate_data_to_projects_from_other_sources( # udpating `project.state` update_state_per_project = [ _projects_service.add_project_states_for_user( - user_id=user_id, - project=prj, - app=app, + user_id=user_id, project=prj, app=app, product_name=product_name ) for prj in db_projects ] @@ -188,7 +188,7 @@ async def list_projects( # pylint: disable=too-many-arguments ) final_projects = await _aggregate_data_to_projects_from_other_sources( - app, db_projects=api_projects, user_id=user_id + app, db_projects=api_projects, user_id=user_id, product_name=product_name ) return final_projects, total_number_projects @@ -235,7 +235,7 @@ async def list_projects_full_depth( # pylint: disable=too-many-arguments ) final_projects = await _aggregate_data_to_projects_from_other_sources( - app, db_projects=api_projects, user_id=user_id + app, db_projects=api_projects, user_id=user_id, product_name=product_name ) return final_projects, total_number_projects diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index 1de25ef85a7..84b410de33a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -281,6 +281,7 @@ async def get_project_for_user( user_id=user_id, project=project, app=app, + product_name=product_name, ) # adds `trashed_by_primary_gid` @@ -1265,7 +1266,7 @@ async def patch_project_node( ) updated_project = await add_project_states_for_user( - user_id=user_id, project=updated_project, app=app + user_id=user_id, project=updated_project, app=app, product_name=product_name ) # 5. if inputs/outputs have been changed all depending nodes shall be notified if {"inputs", "outputs"} & _node_patch_exclude_unset.keys(): @@ -1335,7 +1336,7 @@ async def update_project_node_outputs( pformat(changed_entries), ) updated_project = await add_project_states_for_user( - user_id=user_id, project=updated_project, app=app + user_id=user_id, project=updated_project, app=app, product_name=product_name ) # changed entries come in the form of {node_uuid: {outputs: {changed_key1: value1, changed_key2: value2}}} @@ -1910,6 +1911,7 @@ async def add_project_states_for_user( user_id: int, project: ProjectDict, app: web.Application, + product_name: ProductName, ) -> ProjectDict: _logger.debug( "adding project states for %s with project %s", @@ -1945,6 +1947,7 @@ async def add_project_states_for_user( user_id=user_id, project_uuid=project["uuid"], node_id=NodeID(node_uuid), + product_name=product_name, ) if NodeID(node_uuid) in computational_node_states: node_state = computational_node_states[NodeID(node_uuid)].model_copy( From fce99d6697bbce21ef79307eb89ed023bc9285c0 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:16:50 +0200 Subject: [PATCH 04/11] typo --- .../simcore_service_webserver/projects/_projects_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index 84b410de33a..21af6476e39 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -1376,7 +1376,11 @@ async def _is_service_collaborative( product_name: ProductName, ) -> bool: service = await catalog_service.get_service( - app, user_id=user_id, key=key, version=version, product_name=product_name + app, + user_id=user_id, + service_key=key, + service_version=version, + product_name=product_name, ) return service.get("collaborative", False) is True From 77bab323e06d1f95f7667ca670fdcb15e3b08f1c Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Thu, 21 Aug 2025 10:44:30 +0200 Subject: [PATCH 05/11] moved simcore dependent files to services_metadata_runtime.py --- .../services_metadata_published.py | 5 ++- packages/models-library/tests/test_docker.py | 6 ++-- services/autoscaling/tests/unit/conftest.py | 28 ++++++++-------- .../test_modules_cluster_scaling_dynamic.py | 26 ++++++++------- .../dynamic_sidecar/docker_compose_specs.py | 32 +++++++++---------- .../docker_service_specs/proxy.py | 6 ++-- .../docker_service_specs/sidecar.py | 6 ++-- 7 files changed, 59 insertions(+), 50 deletions(-) diff --git a/packages/models-library/src/models_library/services_metadata_published.py b/packages/models-library/src/models_library/services_metadata_published.py index 8163a91c365..264283b3001 100644 --- a/packages/models-library/src/models_library/services_metadata_published.py +++ b/packages/models-library/src/models_library/services_metadata_published.py @@ -6,6 +6,7 @@ from .basic_types import SemanticVersionStr from .boot_options import BootOption, BootOptions from .emails import LowerCaseEmailStr +from .service_settings_labels import SimcoreServiceLabels from .services_authoring import Author, Badge from .services_base import ServiceBaseDisplay, ServiceKeyVersion from .services_constants import ANY_FILETYPE @@ -87,7 +88,9 @@ } -class ServiceMetaDataPublished(ServiceKeyVersion, ServiceBaseDisplay): +class ServiceMetaDataPublished( + ServiceKeyVersion, ServiceBaseDisplay, SimcoreServiceLabels +): """ Service metadata at publication time diff --git a/packages/models-library/tests/test_docker.py b/packages/models-library/tests/test_docker.py index bcbbff12c36..3fc8f5ba3df 100644 --- a/packages/models-library/tests/test_docker.py +++ b/packages/models-library/tests/test_docker.py @@ -112,9 +112,9 @@ def test_docker_generic_tag(image_name: str, valid: bool): def test_simcore_service_docker_label_keys(obj_data: dict[str, Any]): simcore_service_docker_label_keys = SimcoreContainerLabels.model_validate(obj_data) exported_dict = simcore_service_docker_label_keys.to_simcore_runtime_docker_labels() - assert all( - isinstance(v, str) for v in exported_dict.values() - ), "docker labels must be strings!" + assert all(isinstance(v, str) for v in exported_dict.values()), ( + "docker labels must be strings!" + ) assert all( key.startswith(_SIMCORE_RUNTIME_DOCKER_LABEL_PREFIX) for key in exported_dict ) diff --git a/services/autoscaling/tests/unit/conftest.py b/services/autoscaling/tests/unit/conftest.py index 74c2a0bedc0..3a2f89d7ab2 100644 --- a/services/autoscaling/tests/unit/conftest.py +++ b/services/autoscaling/tests/unit/conftest.py @@ -756,9 +756,9 @@ async def _() -> None: assert tasks, f"no tasks available for {found_service['Spec']['Name']}" assert len(tasks) == 1 service_task = tasks[0] - assert ( - service_task["Status"]["State"] in expected_states - ), f"service {found_service['Spec']['Name']}'s task is {service_task['Status']['State']}" + assert service_task["Status"]["State"] in expected_states, ( + f"service {found_service['Spec']['Name']}'s task is {service_task['Status']['State']}" + ) ctx.logger.info( "%s", f"service {found_service['Spec']['Name']} is now {service_task['Status']['State']} {'.' * number_of_success['count']}", @@ -985,7 +985,9 @@ def _creator( assert ( datetime.timedelta(seconds=10) < app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_TIME_BEFORE_TERMINATION - ), "this tests relies on the fact that the time before termination is above 10 seconds" + ), ( + "this tests relies on the fact that the time before termination is above 10 seconds" + ) assert app_settings.AUTOSCALING_EC2_INSTANCES seconds_delta = ( -datetime.timedelta(seconds=10) @@ -1126,12 +1128,12 @@ def ec2_instances_allowed_types_with_only_1_buffered( allowed_ec2_types.items(), ) ) - assert ( - allowed_ec2_types_with_buffer_defined - ), "one type with buffer is needed for the tests!" - assert ( - len(allowed_ec2_types_with_buffer_defined) == 1 - ), "more than one type with buffer is disallowed in this test!" + assert allowed_ec2_types_with_buffer_defined, ( + "one type with buffer is needed for the tests!" + ) + assert len(allowed_ec2_types_with_buffer_defined) == 1, ( + "more than one type with buffer is disallowed in this test!" + ) return { TypeAdapter(InstanceTypeType).validate_python(k): v for k, v in allowed_ec2_types_with_buffer_defined.items() @@ -1155,9 +1157,9 @@ def _by_buffer_count( filter(_by_buffer_count, allowed_ec2_types.items()) ) assert allowed_ec2_types_with_buffer_defined, "you need one type with buffer" - assert ( - len(allowed_ec2_types_with_buffer_defined) == 1 - ), "more than one type with buffer is disallowed in this test!" + assert len(allowed_ec2_types_with_buffer_defined) == 1, ( + "more than one type with buffer is disallowed in this test!" + ) return next(iter(allowed_ec2_types_with_buffer_defined.values())).buffer_count diff --git a/services/autoscaling/tests/unit/test_modules_cluster_scaling_dynamic.py b/services/autoscaling/tests/unit/test_modules_cluster_scaling_dynamic.py index c1108a54ffa..fea09027a19 100644 --- a/services/autoscaling/tests/unit/test_modules_cluster_scaling_dynamic.py +++ b/services/autoscaling/tests/unit/test_modules_cluster_scaling_dynamic.py @@ -522,9 +522,9 @@ async def _test_cluster_scaling_up_and_down( # noqa: PLR0915 all_instances = await ec2_client.describe_instances(Filters=instance_type_filters) assert not all_instances["Reservations"] - assert ( - scale_up_params.expected_num_instances == 1 - ), "This test is not made to work with more than 1 expected instance. so please adapt if needed" + assert scale_up_params.expected_num_instances == 1, ( + "This test is not made to work with more than 1 expected instance. so please adapt if needed" + ) # create the service(s) created_docker_services = await create_services_batch(scale_up_params) @@ -1283,7 +1283,9 @@ async def test_cluster_adapts_machines_on_the_fly( # noqa: PLR0915 assert ( scale_up_params1.num_services >= app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES - ), "this test requires to run a first batch of more services than the maximum number of instances allowed" + ), ( + "this test requires to run a first batch of more services than the maximum number of instances allowed" + ) # we have nothing running now all_instances = await ec2_client.describe_instances() assert not all_instances["Reservations"] @@ -1500,7 +1502,9 @@ async def test_cluster_adapts_machines_on_the_fly( # noqa: PLR0915 assert "Instances" in reservation1 assert len(reservation1["Instances"]) == ( app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES - ), f"expected {app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES} EC2 instances, found {len(reservation1['Instances'])}" + ), ( + f"expected {app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES} EC2 instances, found {len(reservation1['Instances'])}" + ) for instance in reservation1["Instances"]: assert "InstanceType" in instance assert instance["InstanceType"] == scale_up_params1.expected_instance_type @@ -1514,9 +1518,9 @@ async def test_cluster_adapts_machines_on_the_fly( # noqa: PLR0915 reservation2 = all_instances["Reservations"][1] assert "Instances" in reservation2 - assert ( - len(reservation2["Instances"]) == 1 - ), f"expected 1 EC2 instances, found {len(reservation2['Instances'])}" + assert len(reservation2["Instances"]) == 1, ( + f"expected 1 EC2 instances, found {len(reservation2['Instances'])}" + ) for instance in reservation2["Instances"]: assert "InstanceType" in instance assert instance["InstanceType"] == scale_up_params2.expected_instance_type @@ -2243,9 +2247,9 @@ async def test_warm_buffers_only_replace_hot_buffer_if_service_is_started_issue7 # BUG REPRODUCTION # # start a service that imposes same type as the hot buffer - assert ( - hot_buffer_instance_type == "t2.xlarge" - ), "the test is hard-coded for this type and accordingly resource. If this changed then the resource shall be changed too" + assert hot_buffer_instance_type == "t2.xlarge", ( + "the test is hard-coded for this type and accordingly resource. If this changed then the resource shall be changed too" + ) scale_up_params = _ScaleUpParams( imposed_instance_type=hot_buffer_instance_type, service_resources=Resources( diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_compose_specs.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_compose_specs.py index 9d3ec4f41a3..5366190f13b 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_compose_specs.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_compose_specs.py @@ -296,9 +296,7 @@ async def assemble_spec( # pylint: disable=too-many-arguments # noqa: PLR0913 app.state.settings.DIRECTOR_V2_DOCKER_REGISTRY ) - docker_compose_version = ( - app.state.settings.DYNAMIC_SERVICES.DYNAMIC_SCHEDULER.DYNAMIC_SIDECAR_DOCKER_COMPOSE_VERSION - ) + docker_compose_version = app.state.settings.DYNAMIC_SERVICES.DYNAMIC_SCHEDULER.DYNAMIC_SIDECAR_DOCKER_COMPOSE_VERSION egress_proxy_settings: EgressProxySettings = ( app.state.settings.DYNAMIC_SERVICES.DYNAMIC_SIDECAR_EGRESS_PROXY_SETTINGS @@ -348,19 +346,21 @@ async def assemble_spec( # pylint: disable=too-many-arguments # noqa: PLR0913 service_version=service_version, product_name=product_name, ) - simcore_service_labels = await resolve_and_substitute_session_variables_in_model( - app=app, - model=simcore_service_labels, - # NOTE: at this point all OsparcIdentifiers have to be replaced - # an error will be raised otherwise - safe=False, - user_id=user_id, - product_name=product_name, - product_api_base_url=product_api_base_url, - project_id=project_id, - node_id=node_id, - service_run_id=service_run_id, - wallet_id=wallet_id, + simcore_service_labels = ( + await resolve_and_substitute_session_variables_in_model( + app=app, + model=simcore_service_labels, + # NOTE: at this point all OsparcIdentifiers have to be replaced + # an error will be raised otherwise + safe=False, + user_id=user_id, + product_name=product_name, + product_api_base_url=product_api_base_url, + project_id=project_id, + node_id=node_id, + service_run_id=service_run_id, + wallet_id=wallet_id, + ) ) add_egress_configuration( diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py index df08de3192c..50d0ad43072 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py @@ -35,9 +35,9 @@ def get_dynamic_proxy_spec( The proxy is used to create network isolation from the rest of the platform. """ - assert ( - scheduler_data.product_name is not None - ), "ONLY for legacy. This function should not be called with product_name==None" # nosec + assert scheduler_data.product_name is not None, ( + "ONLY for legacy. This function should not be called with product_name==None" + ) # nosec proxy_settings: DynamicSidecarProxySettings = ( dynamic_services_settings.DYNAMIC_SIDECAR_PROXY_SETTINGS diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/sidecar.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/sidecar.py index 001c6667150..b32d01c6522 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/sidecar.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/sidecar.py @@ -458,9 +458,9 @@ async def get_dynamic_sidecar_spec( # pylint:disable=too-many-arguments# noqa: dynamic_sidecar_settings=dynamic_sidecar_settings, app_settings=app_settings ) - assert ( - scheduler_data.product_name is not None - ), "ONLY for legacy. This function should not be called with product_name==None" # nosec + assert scheduler_data.product_name is not None, ( + "ONLY for legacy. This function should not be called with product_name==None" + ) # nosec standard_simcore_docker_labels: dict[DockerLabelKey, str] = SimcoreContainerLabels( user_id=scheduler_data.user_id, From 3dc6bee7ee8c67720e5bc44cc5ce1006223b8eb2 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:30:19 +0200 Subject: [PATCH 06/11] remove additional base class --- .../src/models_library/services_metadata_published.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/models-library/src/models_library/services_metadata_published.py b/packages/models-library/src/models_library/services_metadata_published.py index 264283b3001..8163a91c365 100644 --- a/packages/models-library/src/models_library/services_metadata_published.py +++ b/packages/models-library/src/models_library/services_metadata_published.py @@ -6,7 +6,6 @@ from .basic_types import SemanticVersionStr from .boot_options import BootOption, BootOptions from .emails import LowerCaseEmailStr -from .service_settings_labels import SimcoreServiceLabels from .services_authoring import Author, Badge from .services_base import ServiceBaseDisplay, ServiceKeyVersion from .services_constants import ANY_FILETYPE @@ -88,9 +87,7 @@ } -class ServiceMetaDataPublished( - ServiceKeyVersion, ServiceBaseDisplay, SimcoreServiceLabels -): +class ServiceMetaDataPublished(ServiceKeyVersion, ServiceBaseDisplay): """ Service metadata at publication time From fc388a3ce557ef1bc2d4f6cf9e2ab7c6895b287e Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:14:26 +0200 Subject: [PATCH 07/11] reverted changes --- .../dynamic_services_service.py | 6 +++ .../_controller/projects_states_rest.py | 5 +-- .../projects/_crud_api_create.py | 5 +-- .../projects/_crud_api_read.py | 8 ++-- .../projects/_projects_service.py | 42 ++++--------------- 5 files changed, 18 insertions(+), 48 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_directorv2/dynamic_services_service.py b/packages/models-library/src/models_library/api_schemas_directorv2/dynamic_services_service.py index bbb57a4d8a1..6dddfa74856 100644 --- a/packages/models-library/src/models_library/api_schemas_directorv2/dynamic_services_service.py +++ b/packages/models-library/src/models_library/api_schemas_directorv2/dynamic_services_service.py @@ -1,5 +1,6 @@ from functools import cached_property from pathlib import Path +from typing import Annotated from pydantic import BaseModel, ConfigDict, Field from pydantic.config import JsonDict @@ -89,6 +90,11 @@ class RunningDynamicServiceDetails(ServiceDetails): alias="service_message", ) + is_collaborative: Annotated[ + bool, + Field(description="True if service allows collaboration (multi-tenant access)"), + ] = False + @staticmethod def _update_json_schema_extra(schema: JsonDict) -> None: schema.update( diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py index 2f0bac3310d..83336ddec8a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py @@ -162,10 +162,7 @@ async def open_project(request: web.Request) -> web.Response: # notify users that project is now opened project = await _projects_service.add_project_states_for_user( - user_id=req_ctx.user_id, - project=project, - app=request.app, - product_name=req_ctx.product_name, + user_id=req_ctx.user_id, project=project, app=request.app ) await _projects_service.notify_project_state_update(request.app, project) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py index 72afac900af..dbe61e99315 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py @@ -430,10 +430,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche ) # Appends state new_project = await _projects_service.add_project_states_for_user( - user_id=user_id, - project=new_project, - app=request.app, - product_name=product_name, + user_id=user_id, project=new_project, app=request.app ) await progress.update() diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index abaf345b48b..de3c36898e2 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -10,7 +10,6 @@ from aiohttp import web from models_library.folders import FolderID, FolderQuery, FolderScope -from models_library.products import ProductName from models_library.projects import ProjectTemplateType from models_library.rest_ordering import OrderBy from models_library.users import UserID @@ -52,7 +51,6 @@ async def _aggregate_data_to_projects_from_other_sources( *, db_projects: list[ProjectDict], user_id: UserID, - product_name: ProductName, ) -> list[ProjectDict]: """ Aggregates data to each project from other sources, first as a batch-update and then as a parallel-update. @@ -75,7 +73,7 @@ async def _aggregate_data_to_projects_from_other_sources( # udpating `project.state` update_state_per_project = [ _projects_service.add_project_states_for_user( - user_id=user_id, project=prj, app=app, product_name=product_name + user_id=user_id, project=prj, app=app ) for prj in db_projects ] @@ -188,7 +186,7 @@ async def list_projects( # pylint: disable=too-many-arguments ) final_projects = await _aggregate_data_to_projects_from_other_sources( - app, db_projects=api_projects, user_id=user_id, product_name=product_name + app, db_projects=api_projects, user_id=user_id ) return final_projects, total_number_projects @@ -235,7 +233,7 @@ async def list_projects_full_depth( # pylint: disable=too-many-arguments ) final_projects = await _aggregate_data_to_projects_from_other_sources( - app, db_projects=api_projects, user_id=user_id, product_name=product_name + app, db_projects=api_projects, user_id=user_id ) return final_projects, total_number_projects diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index 21af6476e39..c24fb5b6c33 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -278,10 +278,7 @@ async def get_project_for_user( # adds state if it is not a template if include_state: project = await add_project_states_for_user( - user_id=user_id, - project=project, - app=app, - product_name=product_name, + user_id=user_id, project=project, app=app ) # adds `trashed_by_primary_gid` @@ -1266,7 +1263,7 @@ async def patch_project_node( ) updated_project = await add_project_states_for_user( - user_id=user_id, project=updated_project, app=app, product_name=product_name + user_id=user_id, project=updated_project, app=app ) # 5. if inputs/outputs have been changed all depending nodes shall be notified if {"inputs", "outputs"} & _node_patch_exclude_unset.keys(): @@ -1336,7 +1333,7 @@ async def update_project_node_outputs( pformat(changed_entries), ) updated_project = await add_project_states_for_user( - user_id=user_id, project=updated_project, app=app, product_name=product_name + user_id=user_id, project=updated_project, app=app ) # changed entries come in the form of {node_uuid: {outputs: {changed_key1: value1, changed_key2: value2}}} @@ -1367,32 +1364,12 @@ async def is_node_id_present_in_any_project_workbench( return await db_legacy.node_id_exists(node_id) -async def _is_service_collaborative( - app: web.Application, - *, - user_id: UserID, - key: ServiceKey, - version: ServiceVersion, - product_name: ProductName, -) -> bool: - service = await catalog_service.get_service( - app, - user_id=user_id, - service_key=key, - service_version=version, - product_name=product_name, - ) - - return service.get("collaborative", False) is True - - async def _get_node_share_state( app: web.Application, *, user_id: UserID, project_uuid: ProjectID, node_id: NodeID, - product_name: ProductName, ) -> NodeShareState: node = await _projects_nodes_repository.get( app, project_id=project_uuid, node_id=node_id @@ -1406,13 +1383,10 @@ async def _get_node_share_state( if isinstance(service, DynamicServiceGet | NodeGet): # service is running - collaborative_service = await _is_service_collaborative( - app, - key=node.key, - version=node.version, - user_id=user_id, - product_name=product_name, - ) + collaborative_service = False + if isinstance(service, DynamicServiceGet): + # only dynamic-sidecar powered services can be collaborative + collaborative_service = service.is_collaborative return NodeShareState( locked=not collaborative_service, @@ -1915,7 +1889,6 @@ async def add_project_states_for_user( user_id: int, project: ProjectDict, app: web.Application, - product_name: ProductName, ) -> ProjectDict: _logger.debug( "adding project states for %s with project %s", @@ -1951,7 +1924,6 @@ async def add_project_states_for_user( user_id=user_id, project_uuid=project["uuid"], node_id=NodeID(node_uuid), - product_name=product_name, ) if NodeID(node_uuid) in computational_node_states: node_state = computational_node_states[NodeID(node_uuid)].model_copy( From 7fc2d92705b5fb8ed4c611bec23fa2edfc006947 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:17:41 +0200 Subject: [PATCH 08/11] pass back collaborative flag --- .../modules/dynamic_sidecar/scheduler/_core/_scheduler_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_scheduler_utils.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_scheduler_utils.py index 3bbd06b7c5c..2c524a4216d 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_scheduler_utils.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_scheduler_utils.py @@ -88,6 +88,7 @@ def create_model_from_scheduler_data( "service_port": scheduler_data.service_port, "service_state": service_state.value, "service_message": service_message, + "is_collaborative": scheduler_data.is_collaborative, } ) From bbc1e6641136727ad91ab4cbcf3ce0ee14b57214 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:40:35 +0200 Subject: [PATCH 09/11] bof --- services/director-v2/tests/unit/test_models_dynamic_services.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/director-v2/tests/unit/test_models_dynamic_services.py b/services/director-v2/tests/unit/test_models_dynamic_services.py index 99a22ece3bb..6981b03c5bb 100644 --- a/services/director-v2/tests/unit/test_models_dynamic_services.py +++ b/services/director-v2/tests/unit/test_models_dynamic_services.py @@ -153,6 +153,7 @@ def test_running_service_details_make_status( "service_host": scheduler_data.service_name, "user_id": scheduler_data.user_id, "service_port": scheduler_data.service_port, + "is_collaborative": scheduler_data.is_collaborative, } assert running_service_details_dict == expected_running_service_details From 032f74471cc3f9e7ec0c09e6e09d4d79b67256c3 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:42:43 +0200 Subject: [PATCH 10/11] bof --- .../simcore_service_webserver/projects/_projects_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py index c24fb5b6c33..06bdcdee33c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_service.py @@ -1383,13 +1383,13 @@ async def _get_node_share_state( if isinstance(service, DynamicServiceGet | NodeGet): # service is running - collaborative_service = False + is_collaborative_service = False if isinstance(service, DynamicServiceGet): # only dynamic-sidecar powered services can be collaborative - collaborative_service = service.is_collaborative + is_collaborative_service = service.is_collaborative return NodeShareState( - locked=not collaborative_service, + locked=not is_collaborative_service, current_user_groupids=[ await users_service.get_user_primary_group_id( app, TypeAdapter(UserID).validate_python(service.user_id) From a18226e3bd49a174431aab2341fa053f50064cf6 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:33:34 +0200 Subject: [PATCH 11/11] missing changes --- services/director-v2/openapi.json | 6 ++++++ services/dynamic-scheduler/openapi.json | 6 ++++++ .../src/simcore_service_webserver/api/v0/openapi.yaml | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/services/director-v2/openapi.json b/services/director-v2/openapi.json index b1beee65c40..701fd9b5b54 100644 --- a/services/director-v2/openapi.json +++ b/services/director-v2/openapi.json @@ -2878,6 +2878,12 @@ ], "title": "Service Message", "description": "additional information related to service state" + }, + "is_collaborative": { + "type": "boolean", + "title": "Is Collaborative", + "description": "True if service allows collaboration (multi-tenant access)", + "default": false } }, "type": "object", diff --git a/services/dynamic-scheduler/openapi.json b/services/dynamic-scheduler/openapi.json index 9f6867c6872..8f234eaad54 100644 --- a/services/dynamic-scheduler/openapi.json +++ b/services/dynamic-scheduler/openapi.json @@ -224,6 +224,12 @@ ], "title": "Service Message", "description": "additional information related to service state" + }, + "is_collaborative": { + "type": "boolean", + "title": "Is Collaborative", + "description": "True if service allows collaboration (multi-tenant access)", + "default": false } }, "type": "object", 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 bdecdf8d20b..6ba4afa38b0 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 @@ -16915,6 +16915,11 @@ components: - type: 'null' title: Service Message description: additional information related to service state + is_collaborative: + type: boolean + title: Is Collaborative + description: True if service allows collaboration (multi-tenant access) + default: false type: object required: - service_key