diff --git a/packages/models-library/src/models_library/api_schemas_webserver/projects_nodes.py b/packages/models-library/src/models_library/api_schemas_webserver/projects_nodes.py index 45e3c87643b..69aafba0962 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/projects_nodes.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/projects_nodes.py @@ -5,7 +5,7 @@ from ..api_schemas_directorv2.dynamic_services import RetrieveDataOut from ..basic_types import PortInt -from ..projects_nodes import InputID, InputsDict +from ..projects_nodes import InputID, InputsDict, PartialNode from ..projects_nodes_io import NodeID from ..services import ServiceKey, ServicePortKey, ServiceVersion from ..services_enums import ServiceState @@ -26,14 +26,26 @@ class NodeCreate(InputSchemaWithoutCamelCase): class NodePatch(InputSchemaWithoutCamelCase): - service_key: ServiceKey | None = Field(default=None, alias="key") - service_version: ServiceVersion | None = Field(default=None, alias="version") - label: str | None = Field(default=None) + service_key: Annotated[ + ServiceKey | None, + Field(alias="key"), + ] = None + service_version: Annotated[ + ServiceVersion | None, + Field(alias="version"), + ] = None + label: str | None = None inputs: Annotated[ InputsDict, Field(default_factory=dict, json_schema_extra={"default": {}}) ] - inputs_required: list[InputID] | None = Field(default=None, alias="inputsRequired") - input_nodes: list[NodeID] | None = Field(default=None, alias="inputNodes") + inputs_required: Annotated[ + list[InputID] | None, + Field(alias="inputsRequired"), + ] = None + input_nodes: Annotated[ + list[NodeID] | None, + Field(alias="inputNodes"), + ] = None progress: Annotated[ float | None, Field( @@ -47,6 +59,13 @@ class NodePatch(InputSchemaWithoutCamelCase): str, Any ] | None = None # NOTE: it is used by frontend for File Picker + def to_domain_model(self) -> PartialNode: + data = self.model_dump( + exclude_unset=True, + by_alias=True, + ) + return PartialNode.model_construct(**data) + class NodeCreated(OutputSchema): node_id: NodeID diff --git a/packages/models-library/src/models_library/projects_nodes.py b/packages/models-library/src/models_library/projects_nodes.py index f7db56b1ded..3fec1406c57 100644 --- a/packages/models-library/src/models_library/projects_nodes.py +++ b/packages/models-library/src/models_library/projects_nodes.py @@ -274,3 +274,9 @@ def _convert_from_enum(cls, v): extra="forbid", populate_by_name=True, ) + + +class PartialNode(Node): + key: Annotated[ServiceKey, Field(default=None)] + version: Annotated[ServiceVersion, Field(default=None)] + label: Annotated[str, Field(default=None)] diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/ecd4eadaa781_extract_workbench_column.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/ecd4eadaa781_extract_workbench_column.py new file mode 100644 index 00000000000..264419b14d6 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/ecd4eadaa781_extract_workbench_column.py @@ -0,0 +1,227 @@ +"""extract workbench column + +Revision ID: ecd4eadaa781 +Revises: a3a58471b0f1 +Create Date: 2025-01-21 13:13:18.256109+00:00 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "ecd4eadaa781" +down_revision = "a3a58471b0f1" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "projects_nodes", + sa.Column( + "key", + sa.String(), + nullable=False, + comment="Distinctive name (based on the Docker registry path)", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "version", sa.String(), nullable=False, comment="Semantic version number" + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "label", sa.String(), nullable=False, comment="Short name used for display" + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "progress", sa.Numeric(), nullable=True, comment="Progress value (0-100)" + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "thumbnail", + sa.String(), + nullable=True, + comment="Url of the latest screenshot", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "input_access", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="Map with key - access level pairs", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "input_nodes", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="IDs of the nodes where is connected to", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "inputs", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="Input properties values", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "inputs_required", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="Required input IDs", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "inputs_units", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="Input units", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "output_nodes", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="Node IDs of those connected to the output", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "outputs", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="Output properties values", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "run_hash", + sa.String(), + nullable=True, + comment="HEX digest of the resolved inputs + outputs hash at the time when the last outputs were generated", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "state", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="State", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "parent", + sa.String(), + nullable=True, + comment="Parent's (group-nodes) node ID", + ), + ) + op.add_column( + "projects_nodes", + sa.Column( + "boot_options", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + comment="Some services provide alternative parameters to be injected at boot time.The user selection should be stored here, and it will overwrite the services's defaults", + ), + ) + # ### end Alembic commands ### + + op.execute( + """ +UPDATE projects_nodes +SET key = subquery.key, + version = subquery.version, + label = subquery.label, + progress = subquery.progress::numeric, + thumbnail = subquery.thumbnail, + input_access = subquery.input_access::jsonb, + input_nodes = subquery.input_nodes::jsonb, + inputs = subquery.inputs::jsonb, + inputs_required = subquery.inputs_required::jsonb, + inputs_units = subquery.inputs_units::jsonb, + output_nodes = subquery.output_nodes::jsonb, + outputs = subquery.outputs::jsonb, + run_hash = subquery.run_hash, + state = subquery.state::jsonb, + parent = subquery.parent, + boot_options = subquery.boot_options::jsonb +FROM ( + SELECT + projects.uuid AS project_id, + js.key AS node_id, + js.value::jsonb ->> 'key' AS key, + js.value::jsonb ->> 'label' AS label, + js.value::jsonb ->> 'version' AS version, + (js.value::jsonb ->> 'progress')::numeric AS progress, + js.value::jsonb ->> 'thumbnail' AS thumbnail, + js.value::jsonb ->> 'inputAccess' AS input_access, + js.value::jsonb ->> 'inputNodes' AS input_nodes, + js.value::jsonb ->> 'inputs' AS inputs, + js.value::jsonb ->> 'inputsRequired' AS inputs_required, + js.value::jsonb ->> 'inputsUnits' AS inputs_units, + js.value::jsonb ->> 'outputNodes' AS output_nodes, + js.value::jsonb ->> 'outputs' AS outputs, + js.value::jsonb ->> 'runHash' AS run_hash, + js.value::jsonb ->> 'state' AS state, + js.value::jsonb ->> 'parent' AS parent, + js.value::jsonb ->> 'bootOptions' AS boot_options + FROM projects, + json_each(projects.workbench) AS js +) AS subquery +WHERE projects_nodes.project_uuid = subquery.project_id +AND projects_nodes.node_id = subquery.node_id; +""" + ) + op.alter_column("projects_nodes", "key", nullable=False) + op.alter_column("projects_nodes", "version", nullable=False) + op.alter_column("projects_nodes", "label", nullable=False) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("projects_nodes", "boot_options") + op.drop_column("projects_nodes", "parent") + op.drop_column("projects_nodes", "state") + op.drop_column("projects_nodes", "run_hash") + op.drop_column("projects_nodes", "outputs") + op.drop_column("projects_nodes", "output_nodes") + op.drop_column("projects_nodes", "inputs_units") + op.drop_column("projects_nodes", "inputs_required") + op.drop_column("projects_nodes", "inputs") + op.drop_column("projects_nodes", "input_nodes") + op.drop_column("projects_nodes", "input_access") + op.drop_column("projects_nodes", "thumbnail") + op.drop_column("projects_nodes", "progress") + op.drop_column("projects_nodes", "label") + op.drop_column("projects_nodes", "version") + op.drop_column("projects_nodes", "key") + # ### end Alembic commands ### 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 f4b569270c4..a5991e7f9db 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 @@ -25,7 +25,7 @@ nullable=False, autoincrement=True, primary_key=True, - doc="Project node index", + doc="Index of the project node", ), sa.Column( "project_uuid", @@ -38,27 +38,123 @@ ), nullable=False, index=True, - doc="The project unique identifier", + doc="Unique identifier of the project", ), sa.Column( "node_id", sa.String, nullable=False, index=True, - doc="The node unique identifier", + doc="Unique identifier of the node", ), sa.Column( "required_resources", JSONB, nullable=False, server_default=sa.text("'{}'::jsonb"), - doc="The node required resources", + doc="Required resources", ), # TIME STAMPS ---- column_created_datetime(timezone=True), column_modified_datetime(timezone=True), + sa.Column( + "key", + sa.String, + nullable=False, + comment="Distinctive name (based on the Docker registry path)", + ), + sa.Column( + "version", + sa.String, + nullable=False, + comment="Semantic version number", + ), + sa.Column( + "label", + sa.String, + nullable=False, + comment="Short name used for display", + ), + sa.Column( + "progress", + sa.Numeric, + nullable=True, + comment="Progress value (0-100)", + ), + sa.Column( + "thumbnail", + sa.String, + nullable=True, + comment="Url of the latest screenshot", + ), + sa.Column( + "input_access", + JSONB, + nullable=True, + comment="Map with key - access level pairs", + ), + sa.Column( + "input_nodes", + JSONB, # Array + nullable=True, + comment="IDs of the nodes where is connected to", + ), + sa.Column( + "inputs", + JSONB, + nullable=True, + comment="Input properties values", + ), + sa.Column( + "inputs_required", + JSONB, # Array + nullable=True, + comment="Required input IDs", + ), + sa.Column( + "inputs_units", + JSONB, + nullable=True, + comment="Input units", + ), + sa.Column( + "output_nodes", + JSONB, # Array + nullable=True, + comment="Node IDs of those connected to the output", + ), + sa.Column( + "outputs", + JSONB, + nullable=True, + comment="Output properties values", + ), + sa.Column( + "run_hash", + sa.String, + nullable=True, + comment="HEX digest of the resolved inputs + outputs hash at the time when the last outputs were generated", + ), + sa.Column( + "state", + JSONB, + nullable=True, + comment="State", + ), + sa.Column( + "parent", + sa.String, + nullable=True, + comment="Parent's (group-nodes) node ID", + ), + sa.Column( + "boot_options", + JSONB, + nullable=True, + comment="Some services provide alternative parameters to be injected at boot time." + "The user selection should be stored here, and it will overwrite the services's defaults", + ), sa.UniqueConstraint("project_uuid", "node_id"), ) - register_modified_datetime_auto_update_trigger(projects_nodes) diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py b/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py index 42b40c778dc..9cab49d27fa 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py @@ -40,10 +40,25 @@ class ProjectNodesDuplicateNodeError(BaseProjectNodesError): class ProjectNodeCreate(BaseModel): node_id: uuid.UUID required_resources: dict[str, Any] = Field(default_factory=dict) + key: str + version: str + label: str + progress: float | None = None + thumbnail: str | None = None + input_access: dict[str, Any] | None = None + input_nodes: list[str] | None = None + inputs: dict[str, Any] | None = None + inputs_units: dict[str, Any] | None = None + output_nodes: list[str] | None = None + outputs: dict[str, Any] | None = None + run_hash: str | None = None + state: dict[str, Any] | None = None + parent: str | None = None + boot_options: dict[str, Any] | None = None @classmethod def get_field_names(cls, *, exclude: set[str]) -> set[str]: - return {name for name in cls.model_fields.keys() if name not in exclude} + return cls.model_fields.keys() - exclude model_config = ConfigDict(frozen=True) @@ -65,7 +80,7 @@ async def add( *, nodes: list[ProjectNodeCreate], ) -> list[ProjectNode]: - """creates a new entry in *projects_nodes* and *projects_to_projects_nodes* tables + """Creates a new entry in *projects_nodes* table NOTE: Do not use this in an asyncio.gather call as this will fail! @@ -83,7 +98,7 @@ async def add( [ { "project_uuid": f"{self.project_uuid}", - **node.model_dump(), + **node.model_dump(exclude_unset=True), } for node in nodes ] diff --git a/packages/postgres-database/src/simcore_postgres_database/webserver_models.py b/packages/postgres-database/src/simcore_postgres_database/webserver_models.py index 571db047cfb..b62a2fd83fe 100644 --- a/packages/postgres-database/src/simcore_postgres_database/webserver_models.py +++ b/packages/postgres-database/src/simcore_postgres_database/webserver_models.py @@ -12,6 +12,7 @@ from .models.groups import GroupType, groups, user_to_groups from .models.products import products from .models.projects import ProjectType, projects +from .models.projects_nodes import projects_nodes from .models.projects_tags import projects_tags from .models.projects_to_wallet import projects_to_wallet from .models.scicrunch_resources import scicrunch_resources @@ -32,6 +33,7 @@ "NodeClass", "products", "projects", + "projects_nodes", "ProjectType", "scicrunch_resources", "StateType", diff --git a/packages/postgres-database/tests/conftest.py b/packages/postgres-database/tests/conftest.py index feb8bfaae97..f9125cc2fce 100644 --- a/packages/postgres-database/tests/conftest.py +++ b/packages/postgres-database/tests/conftest.py @@ -361,6 +361,9 @@ async def _creator(project_uuid: uuid.UUID) -> ProjectNode: fake_node = ProjectNodeCreate( node_id=uuid.uuid4(), required_resources=faker.pydict(allowed_types=(str,)), + key=faker.pystr(), + version=faker.pystr(), + label=faker.pystr(), ) repo = ProjectNodesRepo(project_uuid=project_uuid) created_nodes = await repo.add(connection, nodes=[fake_node]) diff --git a/packages/postgres-database/tests/test_utils_projects_nodes.py b/packages/postgres-database/tests/test_utils_projects_nodes.py index 21c130bcc7d..45362854b42 100644 --- a/packages/postgres-database/tests/test_utils_projects_nodes.py +++ b/packages/postgres-database/tests/test_utils_projects_nodes.py @@ -79,6 +79,9 @@ def _creator() -> ProjectNodeCreate: node = ProjectNodeCreate( node_id=uuid.uuid4(), required_resources=faker.pydict(allowed_types=(str,)), + key=faker.pystr(), + version=faker.pystr(), + label=faker.pystr(), ) assert node return node diff --git a/packages/pytest-simcore/src/pytest_simcore/db_entries_mocks.py b/packages/pytest-simcore/src/pytest_simcore/db_entries_mocks.py index 67e8ec1722d..dc696093ec3 100644 --- a/packages/pytest-simcore/src/pytest_simcore/db_entries_mocks.py +++ b/packages/pytest-simcore/src/pytest_simcore/db_entries_mocks.py @@ -96,7 +96,7 @@ async def creator( inserted_project = ProjectAtDB.model_validate(await result.first()) project_nodes_repo = ProjectNodesRepo(project_uuid=project_uuid) # NOTE: currently no resources is passed until it becomes necessary - default_node_config = {"required_resources": {}} + default_node_config = {"required_resources": {}, "key": faker.pystr(), "version": faker.pystr(), "label": faker.pystr()} if project_nodes_overrides: default_node_config.update(project_nodes_overrides) await project_nodes_repo.add( diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_projects.py b/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_projects.py index 77515cffad5..fbcfaa7b474 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_projects.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/webserver_projects.py @@ -82,8 +82,11 @@ async def create_project( required_resources=ServiceResourcesDictHelpers.model_config[ "json_schema_extra" ]["examples"][0], + key=node_info.get("key"), + version=node_info.get("version"), + label=node_info.get("label"), ) - for node_id in project_data.get("workbench", {}) + for node_id, node_info in project_data.get("workbench", {}).items() }, ) diff --git a/services/web/server/src/simcore_service_webserver/db_listener/_db_comp_tasks_listening_task.py b/services/web/server/src/simcore_service_webserver/db_listener/_db_comp_tasks_listening_task.py index f97f214d4a9..2777fe57e49 100644 --- a/services/web/server/src/simcore_service_webserver/db_listener/_db_comp_tasks_listening_task.py +++ b/services/web/server/src/simcore_service_webserver/db_listener/_db_comp_tasks_listening_task.py @@ -22,7 +22,7 @@ from sqlalchemy.sql import select from ..db.plugin import get_database_engine -from ..projects import exceptions, projects_api +from ..projects import exceptions, projects_service from ..projects.nodes_utils import update_node_outputs from ._utils import convert_state_from_db @@ -47,12 +47,12 @@ async def _update_project_state( new_state: RunningState, node_errors: list[ErrorDict] | None, ) -> None: - project = await projects_api.update_project_node_state( + project = await projects_service.update_project_node_state( app, user_id, project_uuid, node_uuid, new_state ) - await projects_api.notify_project_node_update(app, project, node_uuid, node_errors) - await projects_api.notify_project_state_update(app, project) + await projects_service.notify_project_node_update(app, project, node_uuid, node_errors) + await projects_service.notify_project_state_update(app, project) @dataclass(frozen=True) diff --git a/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py b/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py index 487522963db..62f02f2b1d1 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py +++ b/services/web/server/src/simcore_service_webserver/exporter/_formatter/_sds.py @@ -10,7 +10,7 @@ from ...catalog.client import get_service from ...projects.exceptions import BaseProjectError from ...projects.models import ProjectDict -from ...projects.projects_api import get_project_for_user +from ...projects.projects_service import get_project_for_user from ...scicrunch.db import ResearchResourceRepository from ..exceptions import SDSException from .template_json import write_template_json diff --git a/services/web/server/src/simcore_service_webserver/exporter/_handlers.py b/services/web/server/src/simcore_service_webserver/exporter/_handlers.py index 59321710751..d0e0d975f6c 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/exporter/_handlers.py @@ -15,7 +15,7 @@ from .._constants import RQ_PRODUCT_KEY from .._meta import API_VTAG from ..login.decorators import login_required -from ..projects.projects_api import create_user_notification_cb +from ..projects.projects_service import create_user_notification_cb from ..redis import get_redis_lock_manager_client_sdk from ..security.decorators import permission_required from ..users.api import get_user_fullname diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_service.py b/services/web/server/src/simcore_service_webserver/folders/_folders_service.py index e9960512558..2cd4f0103e9 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_service.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_service.py @@ -16,7 +16,7 @@ from servicelib.common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE from servicelib.utils import fire_and_forget_task -from ..projects.projects_api import submit_delete_project_task +from ..projects.projects_service import submit_delete_project_task from ..users.api import get_user from ..workspaces.api import check_user_workspace_access from ..workspaces.errors import ( diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_core_disconnected.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_core_disconnected.py index 2acdbed9447..68a7c6b55bf 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/_core_disconnected.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_core_disconnected.py @@ -6,7 +6,7 @@ from servicelib.utils import logged_gather from ..projects.exceptions import ProjectLockError, ProjectNotFoundError -from ..projects.projects_api import remove_project_dynamic_services +from ..projects.projects_service import remove_project_dynamic_services from ..redis import get_redis_lock_manager_client from ..resource_manager.registry import ( RedisResourceRegistry, diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_core_guests.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_core_guests.py index 8649d2e2451..f89278ead78 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/_core_guests.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_core_guests.py @@ -13,7 +13,7 @@ from ..projects.db import ProjectDBAPI from ..projects.exceptions import ProjectDeleteError, ProjectNotFoundError -from ..projects.projects_api import get_project_for_user, submit_delete_project_task +from ..projects.projects_service import get_project_for_user, submit_delete_project_task from ..redis import get_redis_lock_manager_client from ..resource_manager.registry import RedisResourceRegistry from ..users import exceptions diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_core_orphans.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_core_orphans.py index 0920aecd168..bcb8af72dfa 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/_core_orphans.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_core_orphans.py @@ -15,7 +15,7 @@ from ..dynamic_scheduler import api as dynamic_scheduler_api from ..projects.api import has_user_project_access_rights -from ..projects.projects_api import ( +from ..projects.projects_service import ( is_node_id_present_in_any_project_workbench, list_node_ids_in_project, ) diff --git a/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py b/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py index 1ba51262d84..4193c6fce7f 100644 --- a/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py +++ b/services/web/server/src/simcore_service_webserver/notifications/_rabbitmq_exclusive_queue_consumers.py @@ -18,7 +18,7 @@ from servicelib.rabbitmq import RabbitMQClient from servicelib.utils import logged_gather -from ..projects import projects_api +from ..projects import projects_service from ..projects.exceptions import ProjectNotFoundError from ..rabbitmq import get_rabbitmq_client from ..socketio.messages import ( @@ -42,7 +42,7 @@ async def _convert_to_node_update_event( app: web.Application, message: ProgressRabbitMessageNode ) -> SocketMessageDict | None: try: - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( app, f"{message.project_id}", message.user_id ) if f"{message.node_id}" in project["workbench"]: diff --git a/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py index c4661005704..04ac3d5ca35 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_comments_handlers.py @@ -30,7 +30,7 @@ from ..login.decorators import login_required from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response -from . import _comments_api, projects_api +from . import _comments_api, projects_service from ._common.models import RequestContext from .exceptions import ProjectInvalidRightsError, ProjectNotFoundError @@ -86,7 +86,7 @@ async def create_project_comment(request: web.Request): body_params = await parse_request_body_as(_ProjectCommentsBodyParams, request) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_uuid}", user_id=req_ctx.user_id, @@ -128,7 +128,7 @@ async def list_project_comments(request: web.Request): ) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_uuid}", user_id=req_ctx.user_id, @@ -176,7 +176,7 @@ async def update_project_comment(request: web.Request): body_params = await parse_request_body_as(_ProjectCommentsBodyParams, request) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_uuid}", user_id=req_ctx.user_id, @@ -205,7 +205,7 @@ async def delete_project_comment(request: web.Request): ) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_uuid}", user_id=req_ctx.user_id, @@ -233,7 +233,7 @@ async def get_project_comment(request: web.Request): ) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_uuid}", user_id=req_ctx.user_id, 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 67d11cfc010..e083e16b28a 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 @@ -10,7 +10,7 @@ from models_library.api_schemas_webserver.projects import ProjectGet from models_library.projects import ProjectID from models_library.projects_access import Owner -from models_library.projects_nodes_io import NodeID, NodeIDStr +from models_library.projects_nodes_io import NodeID from models_library.projects_state import ProjectStatus from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder @@ -39,7 +39,7 @@ from ..workspaces.api import check_user_workspace_access, get_user_workspace from ..workspaces.errors import WorkspaceAccessForbiddenError from . import _folders_db as project_to_folders_db -from . import projects_api +from . import projects_service from ._metadata_api import set_project_ancestors from ._permalink_api import update_or_pop_permalink_in_project from .db import ProjectDBAPI @@ -76,7 +76,7 @@ async def _prepare_project_copy( deep_copy: bool, task_progress: TaskProgress, ) -> tuple[ProjectDict, CopyProjectNodesCoro | None, CopyFileCoro | None]: - source_project = await projects_api.get_project_for_user( + source_project = await projects_service.get_project_for_user( app, project_uuid=f"{src_project_uuid}", user_id=user_id, @@ -134,7 +134,7 @@ async def _copy_project_nodes_from_source_project( db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(app) def _mapped_node_id(node: ProjectNode) -> NodeID: - return NodeID(nodes_map[NodeIDStr(f"{node.node_id}")]) + return NodeID(nodes_map[f"{node.node_id}"]) return { _mapped_node_id(node): ProjectNodeCreate( @@ -157,9 +157,10 @@ async def _copy_files_from_source_project( user_id: UserID, task_progress: TaskProgress, ): - db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(app) + _projects_repository = ProjectDBAPI.get_from_app_context(app) + needs_lock_source_project: bool = ( - await db.get_project_type( + await _projects_repository.get_project_type( TypeAdapter(ProjectID).validate_python(source_project["uuid"]) ) != ProjectTypeDB.TEMPLATE @@ -190,7 +191,7 @@ async def _copy() -> None: owner=Owner( user_id=user_id, **await get_user_fullname(app, user_id=user_id) ), - notification_cb=projects_api.create_user_notification_cb( + notification_cb=projects_service.create_user_notification_cb( user_id, ProjectID(f"{source_project['uuid']}"), app ), )(_copy)() @@ -221,6 +222,9 @@ async def _compose_project_data( app, user_id, node_data["key"], node_data["version"] ) ), + key=node_data.get("key"), + version=node_data.get("version"), + label=node_data.get("label"), ) for node_id, node_data in predefined_project.get("workbench", {}).items() } @@ -267,7 +271,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche """ assert request.app # nosec - db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) + _projects_repository = ProjectDBAPI.get_from_app_context(request.app) new_project: ProjectDict = {} copy_file_coro = None @@ -344,7 +348,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche ) # 3.1 save new project in DB - new_project = await db.insert_project( + new_project = await _projects_repository.insert_project( project=jsonable_encoder(new_project), user_id=user_id, product_name=product_name, @@ -380,7 +384,9 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche # 5. unhide the project if needed since it is now complete if not new_project_was_hidden_before_data_was_copied: - await db.set_hidden_flag(new_project["uuid"], hidden=False) + await _projects_repository.set_hidden_flag( + new_project["uuid"], hidden=False + ) # update the network information in director-v2 await dynamic_scheduler_api.update_projects_networks( @@ -393,9 +399,11 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche request.app, user_id, new_project["uuid"], product_name ) # get the latest state of the project (lastChangeDate for instance) - new_project, _ = await db.get_project(project_uuid=new_project["uuid"]) + new_project, _ = await _projects_repository.get_project( + project_uuid=new_project["uuid"] + ) # Appends state - new_project = await projects_api.add_project_states_for_user( + new_project = await projects_service.add_project_states_for_user( user_id=user_id, project=new_project, is_template=as_template, @@ -407,9 +415,13 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche await update_or_pop_permalink_in_project(request, new_project) # Adds folderId - user_specific_project_data_db = await db.get_user_specific_project_data_db( - project_uuid=new_project["uuid"], - private_workspace_user_id_or_none=user_id if workspace_id is None else None, + user_specific_project_data_db = ( + await _projects_repository.get_user_specific_project_data_db( + project_uuid=new_project["uuid"], + private_workspace_user_id_or_none=user_id + if workspace_id is None + else None, + ) ) new_project["folderId"] = user_specific_project_data_db.folder_id @@ -446,7 +458,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche except (ParentProjectNotFoundError, ParentNodeNotFoundError) as exc: if project_uuid := new_project.get("uuid"): - await projects_api.submit_delete_project_task( + await projects_service.submit_delete_project_task( app=request.app, project_uuid=project_uuid, user_id=user_id, @@ -460,7 +472,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche f"{user_id=}", ) if project_uuid := new_project.get("uuid"): - await projects_api.submit_delete_project_task( + await projects_service.submit_delete_project_task( app=request.app, project_uuid=project_uuid, user_id=user_id, 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 3f9248e7237..9055442a453 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 @@ -20,8 +20,8 @@ from ..catalog.client import get_services_for_user_in_product from ..folders import _folders_repository as folders_db from ..workspaces._workspaces_service import check_user_workspace_access -from . import projects_api from ._permalink_api import update_or_pop_permalink_in_project +from . import projects_service from .db import ProjectDBAPI from .models import ProjectDict, ProjectTypeAPI @@ -34,7 +34,7 @@ async def _append_item( is_template: bool, ): # state - await projects_api.add_project_states_for_user( + await projects_service.add_project_states_for_user( user_id=user_id, project=project, is_template=is_template, diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index 2530db3053d..a46c61c8b73 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -49,7 +49,7 @@ from ..security.decorators import permission_required from ..users.api import get_user_fullname from ..workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError -from . import _crud_api_create, _crud_api_read, projects_api +from . import _crud_api_create, _crud_api_read, projects_service from ._common.models import ProjectPathParams, RequestContext from ._crud_handlers_models import ( ProjectActiveQueryParams, @@ -297,7 +297,7 @@ async def get_active_project(request: web.Request) -> web.Response: data = None if user_active_projects: - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=user_active_projects[0], user_id=req_ctx.user_id, @@ -337,7 +337,7 @@ async def get_project(request: web.Request): ) try: - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -386,7 +386,7 @@ async def get_project(request: web.Request): async def get_project_inactivity(request: web.Request): path_params = parse_request_path_parameters_as(ProjectPathParams, request) - project_inactivity = await projects_api.get_project_inactivity( + project_inactivity = await projects_service.get_project_inactivity( app=request.app, project_id=path_params.project_id ) return web.json_response(Envelope(data=project_inactivity), dumps=json_dumps) @@ -405,7 +405,7 @@ async def patch_project(request: web.Request): path_params = parse_request_path_parameters_as(ProjectPathParams, request) project_patch = await parse_request_body_as(ProjectPatch, request) - await projects_api.patch_project( + await projects_service.patch_project( request.app, user_id=req_ctx.user_id, project_uuid=path_params.project_id, @@ -438,7 +438,7 @@ async def delete_project(request: web.Request): path_params = parse_request_path_parameters_as(ProjectPathParams, request) try: - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -476,7 +476,7 @@ async def delete_project(request: web.Request): reason=f"Project {path_params.project_id} is locked: {project_locked_state=}" ) - await projects_api.submit_delete_project_task( + await projects_service.submit_delete_project_task( request.app, path_params.project_id, req_ctx.user_id, diff --git a/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py index 5511cea2199..44ac05a12c7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_nodes_handlers.py @@ -73,7 +73,7 @@ from ..users.exceptions import UserDefaultWalletNotFoundError from ..utils_aiohttp import envelope_json_response from ..wallets.errors import WalletAccessForbiddenError, WalletNotEnoughCreditsError -from . import nodes_utils, projects_api +from . import nodes_utils, projects_service from ._common.models import ProjectPathParams, RequestContext from ._nodes_api import NodeScreenshot, get_node_screenshots from .exceptions import ( @@ -151,7 +151,7 @@ async def create_node(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(ProjectPathParams, request) body = await parse_request_body_as(NodeCreate, request) - if await projects_api.is_service_deprecated( + if await projects_service.is_service_deprecated( request.app, req_ctx.user_id, body.service_key, @@ -163,13 +163,13 @@ async def create_node(request: web.Request) -> web.Response: ) # ensure the project exists - project_data = await projects_api.get_project_for_user( + project_data = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, ) data = { - "node_id": await projects_api.add_project_node( + "node_id": await projects_service.add_project_node( request, project_data, req_ctx.user_id, @@ -194,13 +194,13 @@ async def get_node(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, ) - if await projects_api.is_project_node_deprecated( + if await projects_service.is_project_node_deprecated( request.app, req_ctx.user_id, project, @@ -232,13 +232,13 @@ async def patch_project_node(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(NodePathParams, request) node_patch = await parse_request_body_as(NodePatch, request) - await projects_api.patch_project_node( + await projects_service.patch_project_node( request.app, product_name=req_ctx.product_name, user_id=req_ctx.user_id, project_id=path_params.project_id, node_id=path_params.node_id, - node_patch=node_patch, + partial_node=node_patch.to_domain_model(), ) return web.json_response(status=status.HTTP_204_NO_CONTENT) @@ -253,12 +253,12 @@ async def delete_node(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, ) - await projects_api.delete_project_node( + await projects_service.delete_project_node( request, path_params.project_id, req_ctx.user_id, @@ -328,7 +328,7 @@ async def start_node(request: web.Request) -> web.Response: req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(NodePathParams, request) - await projects_api.start_project_node( + await projects_service.start_project_node( request, product_name=req_ctx.product_name, user_id=req_ctx.user_id, @@ -438,7 +438,7 @@ async def get_node_resources(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -448,7 +448,7 @@ async def get_node_resources(request: web.Request) -> web.Response: node_id = f"{path_params.node_id}" raise NodeNotFoundError(project_uuid=project_uuid, node_uuid=node_id) - resources: ServiceResourcesDict = await projects_api.get_project_node_resources( + resources: ServiceResourcesDict = await projects_service.get_project_node_resources( request.app, user_id=req_ctx.user_id, project_id=path_params.project_id, @@ -472,7 +472,7 @@ async def replace_node_resources(request: web.Request) -> web.Response: body = await parse_request_body_as(ServiceResourcesDict, request) # ensure the project exists - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -482,7 +482,7 @@ async def replace_node_resources(request: web.Request) -> web.Response: project_uuid=f"{path_params.project_id}", node_uuid=f"{path_params.node_id}" ) try: - new_node_resources = await projects_api.update_project_node_resources( + new_node_resources = await projects_service.update_project_node_resources( request.app, user_id=req_ctx.user_id, project_id=path_params.project_id, @@ -534,7 +534,7 @@ async def get_project_services_access_for_gid( _ServicesAccessQuery, request ) - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -651,7 +651,7 @@ async def list_project_nodes_previews(request: web.Request) -> web.Response: assert req_ctx # nosec nodes_previews: list[_ProjectNodePreview] = [] - project_data = await projects_api.get_project_for_user( + project_data = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -690,7 +690,7 @@ async def get_project_node_preview(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(NodePathParams, request) assert req_ctx # nosec - project_data = await projects_api.get_project_for_user( + project_data = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, diff --git a/services/web/server/src/simcore_service_webserver/projects/_observer.py b/services/web/server/src/simcore_service_webserver/projects/_observer.py index e6267305e46..f830ae40f6f 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_observer.py +++ b/services/web/server/src/simcore_service_webserver/projects/_observer.py @@ -17,7 +17,7 @@ from ..notifications import project_logs from ..resource_manager.user_sessions import PROJECT_ID_KEY, managed_resource -from .projects_api import retrieve_and_notify_project_locked_state +from .projects_service import retrieve_and_notify_project_locked_state _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py index db8be1b9cfd..b134929a8af 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_ports_handlers.py @@ -32,7 +32,7 @@ from ..login.decorators import login_required from ..projects._access_rights_api import check_user_project_permission from ..security.decorators import permission_required -from . import _ports_api, projects_api +from . import _ports_api, projects_service from ._common.models import ProjectPathParams, RequestContext from .db import ProjectDBAPI from .exceptions import ( @@ -81,7 +81,7 @@ async def wrapper(request: web.Request) -> web.Response: async def _get_validated_workbench_model( app: web.Application, project_id: ProjectID, user_id: UserID ) -> dict[NodeID, Node]: - project: ProjectDict = await projects_api.get_project_for_user( + project: ProjectDict = await projects_service.get_project_for_user( app, project_uuid=f"{project_id}", user_id=user_id, diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py index 2748d81061e..a2797993c9a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_pricing_unit_handlers.py @@ -20,7 +20,7 @@ from ..resource_usage import api as rut_api from ..security.decorators import permission_required from ..utils_aiohttp import envelope_json_response -from . import projects_api +from . import projects_service from ._common.models import RequestContext from ._nodes_handlers import NodePathParams from .db import ProjectDBAPI @@ -68,7 +68,7 @@ async def get_project_node_pricing_unit(request: web.Request): path_params = parse_request_path_parameters_as(NodePathParams, request) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -117,7 +117,7 @@ async def connect_pricing_unit_to_project_node(request: web.Request): ) # ensure the project exists - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -143,7 +143,7 @@ async def connect_pricing_unit_to_project_node(request: web.Request): node_data = project["workbench"][NodeIDStr(f"{path_params.node_id}")] - await projects_api.update_project_node_resources_from_hardware_info( + await projects_service.update_project_node_resources_from_hardware_info( request.app, user_id=req_ctx.user_id, project_id=path_params.project_id, diff --git a/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_repository.py b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_repository.py new file mode 100644 index 00000000000..cd6880732e0 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/projects/_projects_nodes_repository.py @@ -0,0 +1,85 @@ +import logging + +import sqlalchemy as sa + +from aiohttp import web +from models_library.projects import ProjectID +from models_library.projects_nodes import Node, PartialNode +from models_library.projects_nodes_io import NodeID +from simcore_postgres_database.utils_repos import transaction_context +from simcore_postgres_database.webserver_models import projects_nodes +from sqlalchemy.ext.asyncio import AsyncConnection + +from .exceptions import NodeNotFoundError +from ..db.plugin import get_asyncpg_engine + +_logger = logging.getLogger(__name__) + + +_SELECTION_PROJECTS_NODES_DB_ARGS = [ + projects_nodes.c.key, + projects_nodes.c.version, + projects_nodes.c.label, + projects_nodes.c.progress, + projects_nodes.c.thumbnail, + projects_nodes.c.input_access, + projects_nodes.c.input_nodes, + projects_nodes.c.inputs, + projects_nodes.c.inputs_required, + projects_nodes.c.inputs_units, + projects_nodes.c.output_nodes, + projects_nodes.c.outputs, + projects_nodes.c.run_hash, + projects_nodes.c.state, + projects_nodes.c.parent, + projects_nodes.c.boot_options, +] + + +async def get( + app: web.Application, + connection: AsyncConnection | None = None, + *, + project_id: ProjectID, + node_id: NodeID, +) -> Node: + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + get_stmt = sa.select( + *_SELECTION_PROJECTS_NODES_DB_ARGS + ).where( + (projects_nodes.c.project_uuid == f"{project_id}") + & (projects_nodes.c.node_id == f"{node_id}") + ) + + result = await conn.stream(get_stmt) + assert result # nosec + + row = await result.first() + if row is None: + raise NodeNotFoundError( + project_uuid=f"{project_id}", + node_uuid=f"{node_id}" + ) + assert row # nosec + return Node.model_validate(row, from_attributes=True) + + +async def update( + app: web.Application, + connection: AsyncConnection | None = None, + *, + project_id: ProjectID, + node_id: NodeID, + partial_node: PartialNode, +) -> None: + values = partial_node.model_dump(mode="json", exclude_unset=True) + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + await conn.stream( + projects_nodes.update() + .values(**values) + .where( + (projects_nodes.c.project_uuid == f"{project_id}") + & (projects_nodes.c.node_id == f"{node_id}") + ) + ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py index d1849918804..956226d7f32 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_states_handlers.py @@ -34,8 +34,8 @@ from ..users.exceptions import UserDefaultWalletNotFoundError from ..utils_aiohttp import envelope_json_response from ..wallets.errors import WalletNotEnoughCreditsError -from . import api as projects_service -from . import projects_api +from . import api as projects_api +from . import projects_service from ._common.models import ProjectPathParams, RequestContext from .exceptions import ( DefaultPricingUnitNotFoundError, @@ -112,7 +112,7 @@ async def open_project(request: web.Request) -> web.Response: raise web.HTTPBadRequest(reason="Invalid request body") from exc try: - project_type: ProjectType = await projects_api.get_project_type( + project_type: ProjectType = await projects_service.get_project_type( request.app, path_params.project_id ) user_role: UserRole = await api.get_user_role( @@ -122,7 +122,7 @@ async def open_project(request: web.Request) -> web.Response: # only USERS/TESTERS can do that raise web.HTTPForbidden(reason="Wrong user role to open/edit a template") - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -132,7 +132,7 @@ async def open_project(request: web.Request) -> web.Response: ), ) - await projects_service.check_project_financial_status( + await projects_api.check_project_financial_status( request.app, project_id=path_params.project_id, product_name=req_ctx.product_name, @@ -140,7 +140,7 @@ async def open_project(request: web.Request) -> web.Response: product: Product = get_current_product(request) - if not await projects_api.try_open_project_for_user( + if not await projects_service.try_open_project_for_user( req_ctx.user_id, project_uuid=path_params.project_id, client_session_id=client_session_id, @@ -150,7 +150,7 @@ async def open_project(request: web.Request) -> web.Response: raise HTTPLockedError(reason="Project is locked, try later") # the project can be opened, let's update its product links - await projects_api.update_project_linked_product( + await projects_service.update_project_linked_product( request.app, path_params.project_id, req_ctx.product_name ) @@ -163,30 +163,30 @@ async def open_project(request: web.Request) -> web.Response: # NOTE: this method raises that exception when the number of dynamic # services in the project is highter than the maximum allowed per project # the project shall still open though. - await projects_api.run_project_dynamic_services( + await projects_service.run_project_dynamic_services( request, project, req_ctx.user_id, req_ctx.product_name ) # and let's update the project last change timestamp - await projects_api.update_project_last_change_timestamp( + await projects_service.update_project_last_change_timestamp( request.app, path_params.project_id ) # notify users that project is now opened - project = await projects_api.add_project_states_for_user( + project = await projects_service.add_project_states_for_user( user_id=req_ctx.user_id, project=project, is_template=False, app=request.app, ) - await projects_api.notify_project_state_update(request.app, project) + await projects_service.notify_project_state_update(request.app, project) return envelope_json_response(ProjectGet.from_domain_model(project)) except DirectorServiceError as exc: # there was an issue while accessing the director-v2/director-v0 # ensure the project is closed again - await projects_api.try_close_project_for_user( + await projects_service.try_close_project_for_user( user_id=req_ctx.user_id, project_uuid=f"{path_params.project_id}", client_session_id=client_session_id, @@ -220,13 +220,13 @@ async def close_project(request: web.Request) -> web.Response: raise web.HTTPBadRequest(reason="Invalid request body") from exc # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, include_state=False, ) - await projects_api.try_close_project_for_user( + await projects_service.try_close_project_for_user( req_ctx.user_id, f"{path_params.project_id}", client_session_id, @@ -252,7 +252,7 @@ async def get_project_state(request: web.Request) -> web.Response: path_params = parse_request_path_parameters_as(ProjectPathParams, request) # check that project exists and queries state - validated_project = await projects_api.get_project_for_user( + validated_project = await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, diff --git a/services/web/server/src/simcore_service_webserver/projects/_trash_service.py b/services/web/server/src/simcore_service_webserver/projects/_trash_service.py index 13e07c51475..e535d387010 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_trash_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_trash_service.py @@ -12,7 +12,7 @@ from ..director_v2 import api as director_v2_api from ..dynamic_scheduler import api as dynamic_scheduler_api -from . import projects_api +from . import projects_service from ._access_rights_api import check_user_project_permission from .exceptions import ProjectRunningConflictError from .models import ProjectPatchExtended @@ -67,7 +67,7 @@ async def _schedule(): director_v2_api.stop_pipeline( app, user_id=user_id, project_id=project_id ), - projects_api.remove_project_dynamic_services( + projects_service.remove_project_dynamic_services( user_id=user_id, project_uuid=f"{project_id}", app=app, @@ -89,7 +89,7 @@ async def _schedule(): product_name=product_name, ) - await projects_api.patch_project( + await projects_service.patch_project( app, user_id=user_id, product_name=product_name, @@ -108,7 +108,7 @@ async def untrash_project( project_id: ProjectID, ): # NOTE: check_user_project_permission is inside projects_api.patch_project - await projects_api.patch_project( + await projects_service.patch_project( app, user_id=user_id, product_name=product_name, diff --git a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py index 9dc9668a681..346958c3487 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_wallets_handlers.py @@ -25,7 +25,7 @@ from ..security.decorators import permission_required from ..wallets.errors import WalletAccessForbiddenError, WalletNotFoundError from . import _wallets_api as wallets_api -from . import projects_api +from . import projects_service from ._common.models import ProjectPathParams, RequestContext from .exceptions import ( ProjectInDebtCanNotChangeWalletError, @@ -74,7 +74,7 @@ async def get_project_wallet(request: web.Request): path_params = parse_request_path_parameters_as(ProjectPathParams, request) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -105,7 +105,7 @@ async def connect_wallet_to_project(request: web.Request): path_params = parse_request_path_parameters_as(_ProjectWalletPathParams, request) # ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, @@ -141,7 +141,7 @@ async def pay_project_debt(request: web.Request): body_params = await parse_request_body_as(_PayProjectDebtBody, request) # Ensure the project exists - await projects_api.get_project_for_user( + await projects_service.get_project_for_user( request.app, project_uuid=f"{path_params.project_id}", user_id=req_ctx.user_id, diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index 47704f74e2f..9ba0df06607 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -220,19 +220,27 @@ def _reraise_if_not_unique_uuid_error(err: UniqueViolation): if project_nodes is None: project_nodes = { NodeID(node_id): ProjectNodeCreate( - node_id=NodeID(node_id), required_resources={} + node_id=NodeID(node_id), + required_resources={}, + key=node_info.get("key"), + version=node_info.get("version"), + label=node_info.get("label"), ) - for node_id in selected_values["workbench"] + for node_id, node_info in selected_values["workbench"].items() } nodes = [ project_nodes.get( NodeID(node_id), ProjectNodeCreate( - node_id=NodeID(node_id), required_resources={} + node_id=NodeID(node_id), + required_resources={}, + key=node_info.get("key"), + version=node_info.get("version"), + label=node_info.get("label"), ), ) - for node_id in selected_values["workbench"] + for node_id, node_info in selected_values["workbench"].items() ] await project_nodes_repo.add(conn, nodes=nodes) return selected_values diff --git a/services/web/server/src/simcore_service_webserver/projects/models.py b/services/web/server/src/simcore_service_webserver/projects/models.py index 8354bdff549..eab20e2f848 100644 --- a/services/web/server/src/simcore_service_webserver/projects/models.py +++ b/services/web/server/src/simcore_service_webserver/projects/models.py @@ -106,9 +106,3 @@ def to_domain_model(self) -> dict[str, Any]: self.model_dump(exclude_unset=True, by_alias=False), rename={"trashed_at": "trashed"}, ) - - -__all__: tuple[str, ...] = ( - "ProjectDict", - "ProjectProxy", -) diff --git a/services/web/server/src/simcore_service_webserver/projects/nodes_utils.py b/services/web/server/src/simcore_service_webserver/projects/nodes_utils.py index 41925557a20..32531914163 100644 --- a/services/web/server/src/simcore_service_webserver/projects/nodes_utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/nodes_utils.py @@ -12,7 +12,7 @@ from servicelib.logging_utils import log_decorator from servicelib.utils import fire_and_forget_task, logged_gather -from . import projects_api +from . import projects_service from .utils import get_frontend_node_outputs_changes log = logging.getLogger(__name__) @@ -46,7 +46,7 @@ async def update_node_outputs( ui_changed_keys: set[str] | None, ) -> None: # the new outputs might be {}, or {key_name: payload} - project, keys_changed = await projects_api.update_project_node_outputs( + project, keys_changed = await projects_service.update_project_node_outputs( app, user_id, project_uuid, @@ -55,14 +55,14 @@ async def update_node_outputs( new_run_hash=run_hash, ) - await projects_api.notify_project_node_update( + await projects_service.notify_project_node_update( app, project, node_uuid, errors=node_errors ) # get depending node and notify for these ones as well depending_node_uuids = await project_get_depending_nodes(project, node_uuid) await logged_gather( *[ - projects_api.notify_project_node_update(app, project, nid, errors=None) + projects_service.notify_project_node_update(app, project, nid, errors=None) for nid in depending_node_uuids ] ) @@ -86,7 +86,7 @@ async def update_node_outputs( ) # fire&forget to notify connected nodes to retrieve its inputs **if necessary** - await projects_api.post_trigger_connected_service_retrieve( + await projects_service.post_trigger_connected_service_retrieve( app=app, project=project, updated_node_uuid=f"{node_uuid}", changed_keys=keys ) diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_api.py b/services/web/server/src/simcore_service_webserver/projects/projects_service.py similarity index 97% rename from services/web/server/src/simcore_service_webserver/projects/projects_api.py rename to services/web/server/src/simcore_service_webserver/projects/projects_service.py index 8d421b9ad42..e34c784da73 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_service.py @@ -31,14 +31,13 @@ DynamicServiceStop, ) from models_library.api_schemas_webserver.projects import ProjectPatch -from models_library.api_schemas_webserver.projects_nodes import NodePatch from models_library.basic_types import KeyIDStr from models_library.errors import ErrorDict from models_library.groups import GroupID from models_library.products import ProductName from models_library.projects import Project, ProjectID, ProjectIDStr from models_library.projects_access import Owner -from models_library.projects_nodes import Node +from models_library.projects_nodes import Node, NodeState, PartialNode from models_library.projects_nodes_io import NodeID, NodeIDStr, PortLink from models_library.projects_state import ( ProjectLocked, @@ -125,7 +124,7 @@ from ..wallets import api as wallets_api from ..wallets.errors import WalletNotEnoughCreditsError from ..workspaces import _workspaces_repository as workspaces_db -from . import _crud_api_delete, _nodes_api, _projects_db +from . import _crud_api_delete, _nodes_api, _projects_db, _projects_nodes_repository from ._access_rights_api import ( check_user_project_permission, has_user_project_access_rights, @@ -381,9 +380,9 @@ async def _get_default_pricing_and_hardware_info( ) -_MACHINE_TOTAL_RAM_SAFE_MARGIN_RATIO: Final[float] = ( - 0.1 # NOTE: machines always have less available RAM than advertised -) +_MACHINE_TOTAL_RAM_SAFE_MARGIN_RATIO: Final[ + float +] = 0.1 # NOTE: machines always have less available RAM than advertised _SIDECARS_OPS_SAFE_RAM_MARGIN: Final[ByteSize] = TypeAdapter(ByteSize).validate_python( "1GiB" ) @@ -789,13 +788,17 @@ async def add_project_node( default_resources = await catalog_client.get_service_resources( request.app, user_id, service_key, service_version ) - db: ProjectDBAPI = request.app[APP_PROJECT_DBAPI] + db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) assert db # nosec await db.add_project_node( user_id, ProjectID(project["uuid"]), ProjectNodeCreate( - node_id=node_uuid, required_resources=jsonable_encoder(default_resources) + node_id=node_uuid, + required_resources=jsonable_encoder(default_resources), + key=service_key, + version=service_version, + label=service_key.split("/")[-1], ), Node.model_validate( { @@ -972,6 +975,8 @@ async def update_project_node_state( permission="write", # NOTE: MD: before only read was sufficient, double check this ) + # Delete this once workbench is removed from the projects table + # See: https://github.com/ITISFoundation/osparc-simcore/issues/7046 updated_project, _ = await db.update_project_node_data( user_id=user_id, project_uuid=project_id, @@ -979,6 +984,16 @@ async def update_project_node_state( product_name=None, new_node_data={"state": {"currentStatus": new_state}}, ) + + await _projects_nodes_repository.update( + app, + project_id=project_id, + node_id=node_id, + partial_node=PartialNode.model_construct( + state=NodeState(currentStatus=RunningState(new_state)) + ), + ) + return await add_project_states_for_user( user_id=user_id, project=updated_project, is_template=False, app=app ) @@ -996,12 +1011,13 @@ async def patch_project_node( user_id: UserID, project_id: ProjectID, node_id: NodeID, - node_patch: NodePatch, + partial_node: PartialNode, ) -> None: - _node_patch_exclude_unset: dict[str, Any] = jsonable_encoder( - node_patch, exclude_unset=True, by_alias=True + _node_patch_exclude_unset: dict[str, Any] = partial_node.model_dump( + mode="json", exclude_unset=True, by_alias=True ) - db: ProjectDBAPI = app[APP_PROJECT_DBAPI] + + _projects_repository = ProjectDBAPI.get_from_app_context(app) # 1. Check user permissions await check_user_project_permission( @@ -1014,7 +1030,9 @@ async def patch_project_node( # 2. If patching service key or version make sure it's valid if _node_patch_exclude_unset.get("key") or _node_patch_exclude_unset.get("version"): - _project, _ = await db.get_project(project_uuid=f"{project_id}") + _project, _ = await _projects_repository.get_project( + project_uuid=f"{project_id}" + ) _project_node_data = _project["workbench"][f"{node_id}"] _service_key = _node_patch_exclude_unset.get("key", _project_node_data["key"]) @@ -1031,7 +1049,7 @@ async def patch_project_node( ) # 3. Patch the project node - updated_project, _ = await db.update_project_node_data( + updated_project, _ = await _projects_repository.update_project_node_data( user_id=user_id, project_uuid=project_id, node_id=node_id, @@ -1039,6 +1057,13 @@ async def patch_project_node( new_node_data=_node_patch_exclude_unset, ) + await _projects_nodes_repository.update( + app, + project_id=project_id, + node_id=node_id, + partial_node=partial_node, + ) + # 4. Make calls to director-v2 to keep data in sync (ex. comp_tasks DB table) await director_v2_api.create_or_update_pipeline( app, user_id, project_id, product_name=product_name @@ -1100,6 +1125,15 @@ async def update_project_node_outputs( new_node_data={"outputs": new_outputs, "runHash": new_run_hash}, ) + await _projects_nodes_repository.update( + app, + project_id=project_id, + node_id=node_id, + partial_node=PartialNode.model_construct( + outputs=new_outputs, run_hash=new_run_hash + ), + ) + log.debug( "patched project %s, following entries changed: %s", project_id, diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py index 3c80c832246..788ca886593 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_projects.py @@ -23,7 +23,7 @@ from ..projects.db import ProjectDBAPI from ..projects.exceptions import ProjectInvalidRightsError, ProjectNotFoundError -from ..projects.projects_api import get_project_for_user +from ..projects.projects_service import get_project_for_user from ..utils import now_str from ._core import compose_uuid_from from ._models import FileParams, ServiceInfo, ViewerInfo diff --git a/services/web/server/tests/unit/with_dbs/02/conftest.py b/services/web/server/tests/unit/with_dbs/02/conftest.py index 148d6315d8d..25be7db87c8 100644 --- a/services/web/server/tests/unit/with_dbs/02/conftest.py +++ b/services/web/server/tests/unit/with_dbs/02/conftest.py @@ -92,12 +92,12 @@ def mock_catalog_api( ) -> dict[str, mock.Mock]: return { "get_service_resources": mocker.patch( - "simcore_service_webserver.projects.projects_api.catalog_client.get_service_resources", + "simcore_service_webserver.projects.projects_service.catalog_client.get_service_resources", return_value=mock_service_resources, autospec=True, ), "get_service": mocker.patch( - "simcore_service_webserver.projects.projects_api.catalog_client.get_service", + "simcore_service_webserver.projects.projects_service.catalog_client.get_service", return_value=mock_service, autospec=True, ), @@ -374,7 +374,7 @@ def mock_get_total_project_dynamic_nodes_creation_interval( ) -> None: _VERY_LONG_LOCK_TIMEOUT_S: Final[float] = 300 mocker.patch( - "simcore_service_webserver.projects.projects_api._nodes_api" + "simcore_service_webserver.projects.projects_service._nodes_api" ".get_total_project_dynamic_nodes_creation_interval", return_value=_VERY_LONG_LOCK_TIMEOUT_S, ) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py index f9af27e9398..1f6f18cc00a 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_handlers__patch.py @@ -47,7 +47,7 @@ def mock_project_uses_available_services(mocker: MockerFixture): @pytest.fixture def mock_catalog_rpc_check_for_service(mocker: MockerFixture): mocker.patch( - "simcore_service_webserver.projects.projects_api.catalog_rpc.check_for_service", + "simcore_service_webserver.projects.projects_service.catalog_rpc.check_for_service", spec=True, return_value=True, ) @@ -56,7 +56,7 @@ def mock_catalog_rpc_check_for_service(mocker: MockerFixture): @pytest.fixture def mocked_notify_project_node_update(mocker: MockerFixture): return mocker.patch( - "simcore_service_webserver.projects.projects_api.notify_project_node_update", + "simcore_service_webserver.projects.projects_service.notify_project_node_update", ) @@ -362,14 +362,14 @@ async def test_patch_project_node_service_key_with_error( _patch_version = {"version": "2.0.9"} with mocker.patch( - "simcore_service_webserver.projects.projects_api.catalog_rpc.check_for_service", + "simcore_service_webserver.projects.projects_service.catalog_rpc.check_for_service", side_effect=CatalogForbiddenError(name="test"), ): resp = await client.patch(f"{base_url}", json=_patch_version) assert resp.status == status.HTTP_403_FORBIDDEN with mocker.patch( - "simcore_service_webserver.projects.projects_api.catalog_rpc.check_for_service", + "simcore_service_webserver.projects.projects_service.catalog_rpc.check_for_service", side_effect=CatalogItemNotFoundError(name="test"), ): resp = await client.patch(f"{base_url}", json=_patch_version) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py index 1ebd06d7392..dad139ec8bb 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_nodes_pricing_unit_handlers.py @@ -136,7 +136,7 @@ def _fake_instance_type_details( ] return mocker.patch( - "simcore_service_webserver.projects.projects_api.get_instance_type_details", + "simcore_service_webserver.projects.projects_service.get_instance_type_details", side_effect=_fake_instance_type_details, ) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py index a7e361cd2ff..3963a10bf7b 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py @@ -1073,7 +1073,7 @@ async def test_project_node_lifetime( # noqa: PLR0915 create_dynamic_service_mock: Callable[..., Awaitable[DynamicServiceGet]], ): mock_storage_api_delete_data_folders_of_project_node = mocker.patch( - "simcore_service_webserver.projects._crud_handlers.projects_api.storage_api.delete_data_folders_of_project_node", + "simcore_service_webserver.projects._crud_handlers.projects_service.storage_api.delete_data_folders_of_project_node", return_value="", ) assert client.app diff --git a/services/web/server/tests/unit/with_dbs/03/test_project_db.py b/services/web/server/tests/unit/with_dbs/03/test_project_db.py index 0d9ef69bcbe..d62cac76a51 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_project_db.py +++ b/services/web/server/tests/unit/with_dbs/03/test_project_db.py @@ -39,7 +39,7 @@ ProjectNotFoundError, ) from simcore_service_webserver.projects.models import ProjectDict -from simcore_service_webserver.projects.projects_api import ( +from simcore_service_webserver.projects.projects_service import ( _check_project_node_has_all_required_inputs, ) from simcore_service_webserver.users.exceptions import UserNotFoundError 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 6c38f65770d..08d9d4f864d 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 @@ -76,7 +76,7 @@ async def test_trash_projects( # noqa: PLR0915 # this test should have no errors stopping services mock_remove_dynamic_services = mocker.patch( - "simcore_service_webserver.projects._trash_service.projects_api.remove_project_dynamic_services", + "simcore_service_webserver.projects._trash_service.projects_service.remove_project_dynamic_services", autospec=True, ) mock_stop_pipeline = mocker.patch( diff --git a/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py b/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py index d9fc7213a63..4b6504bdfd3 100644 --- a/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py +++ b/services/web/server/tests/unit/with_dbs/03/version_control/conftest.py @@ -141,12 +141,12 @@ def request_update_project( mocker: MockerFixture, ) -> Callable[[TestClient, UUID], Awaitable]: mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.projects_api.is_service_deprecated", + "simcore_service_webserver.projects._nodes_handlers.projects_service.is_service_deprecated", autospec=True, return_value=False, ) mocker.patch( - "simcore_service_webserver.projects._nodes_handlers.projects_api.catalog_client.get_service_resources", + "simcore_service_webserver.projects._nodes_handlers.projects_service.catalog_client.get_service_resources", autospec=True, return_value=ServiceResourcesDict(), ) @@ -218,11 +218,11 @@ async def request_delete_project( mocker: MockerFixture, ) -> AsyncIterator[Callable[[TestClient, UUID], Awaitable]]: director_v2_api_delete_pipeline: mock.AsyncMock = mocker.patch( - "simcore_service_webserver.projects.projects_api.director_v2_api.delete_pipeline", + "simcore_service_webserver.projects.projects_service.director_v2_api.delete_pipeline", autospec=True, ) dynamic_scheduler_api_stop_dynamic_services_in_project: mock.AsyncMock = mocker.patch( - "simcore_service_webserver.projects.projects_api.dynamic_scheduler_api.stop_dynamic_services_in_project", + "simcore_service_webserver.projects.projects_service.dynamic_scheduler_api.stop_dynamic_services_in_project", autospec=True, ) fire_and_forget_call_to_storage: mock.Mock = mocker.patch( diff --git a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py index c5ca46bf52c..9ae6a29c127 100644 --- a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py +++ b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_core.py @@ -9,7 +9,7 @@ from aiohttp import web from aiohttp.test_utils import TestClient, make_mocked_request from simcore_service_webserver._constants import RQT_USERID_KEY -from simcore_service_webserver.projects import projects_api +from simcore_service_webserver.projects import projects_service from simcore_service_webserver.projects.models import ProjectDict from simcore_service_webserver.version_control._core import ( checkout_checkpoint, @@ -79,7 +79,7 @@ async def test_workflow( checkpoint_co = await checkout_checkpoint(vc_repo, project_uuid, checkpoint1.id) assert checkpoint1 == checkpoint_co - project = await projects_api.get_project_for_user( + project = await projects_service.get_project_for_user( aiohttp_mocked_request.app, str(project_uuid), user_id ) assert project["workbench"] == user_project["workbench"] diff --git a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py index a1faf49d35f..df0d767a9e9 100644 --- a/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py +++ b/services/web/server/tests/unit/with_dbs/03/version_control/test_version_control_handlers.py @@ -241,9 +241,9 @@ async def test_delete_project_and_repo( # TMP fix here waits ------------ # FIXME: mark as deleted, still gets entrypoints!! - from simcore_service_webserver.projects import projects_api + from simcore_service_webserver.projects import projects_service - delete_task = projects_api.get_delete_project_task(project_uuid, user_id) + delete_task = projects_service.get_delete_project_task(project_uuid, user_id) assert delete_task await delete_task # -------------------------------- diff --git a/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py b/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py index 03e30daedc4..f4b2df540ae 100644 --- a/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py +++ b/services/web/server/tests/unit/with_dbs/04/folders/test_folders.py @@ -387,7 +387,7 @@ def mock_storage_delete_data_folders(mocker: MockerFixture) -> mock.Mock: autospec=True, ) mocker.patch( - "simcore_service_webserver.projects.projects_api.remove_project_dynamic_services", + "simcore_service_webserver.projects.projects_service.remove_project_dynamic_services", autospec=True, ) mocker.patch( diff --git a/services/web/server/tests/unit/with_dbs/04/garbage_collector/test_resource_manager.py b/services/web/server/tests/unit/with_dbs/04/garbage_collector/test_resource_manager.py index d4aee7eb58f..79969076f92 100644 --- a/services/web/server/tests/unit/with_dbs/04/garbage_collector/test_resource_manager.py +++ b/services/web/server/tests/unit/with_dbs/04/garbage_collector/test_resource_manager.py @@ -46,7 +46,7 @@ from simcore_service_webserver.products.plugin import setup_products from simcore_service_webserver.projects.exceptions import ProjectNotFoundError from simcore_service_webserver.projects.plugin import setup_projects -from simcore_service_webserver.projects.projects_api import ( +from simcore_service_webserver.projects.projects_service import ( remove_project_dynamic_services, submit_delete_project_task, ) @@ -653,7 +653,7 @@ async def test_interactive_services_remain_after_websocket_reconnection_from_2_t async def mocked_notification_system(mocker): mocks = {} mocked_notification_system = mocker.patch( - "simcore_service_webserver.projects.projects_api.retrieve_and_notify_project_locked_state", + "simcore_service_webserver.projects.projects_service.retrieve_and_notify_project_locked_state", return_value=Future(), ) mocked_notification_system.return_value.set_result("") diff --git a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py index 29c3251bbad..4a3194ca9a4 100644 --- a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py +++ b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_handlers.py @@ -302,7 +302,7 @@ def mocks_on_projects_api(mocker) -> None: All projects in this module are UNLOCKED """ mocker.patch( - "simcore_service_webserver.projects.projects_api._get_project_lock_state", + "simcore_service_webserver.projects.projects_service._get_project_lock_state", return_value=ProjectLocked(value=False, status=ProjectStatus.CLOSED), ) diff --git a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_projects.py b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_projects.py index 1488ea3bbc0..b1c7e7259d2 100644 --- a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_projects.py +++ b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_projects.py @@ -18,7 +18,7 @@ from pytest_simcore.helpers.webserver_login import NewUser from pytest_simcore.helpers.webserver_projects import delete_all_projects from simcore_service_webserver.groups.api import auto_add_user_to_groups -from simcore_service_webserver.projects.projects_api import get_project_for_user +from simcore_service_webserver.projects.projects_service import get_project_for_user from simcore_service_webserver.studies_dispatcher._models import ServiceInfo from simcore_service_webserver.studies_dispatcher._projects import ( UserInfo, diff --git a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_studies_access.py b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_studies_access.py index bcbcbb4c6f7..10a9367d101 100644 --- a/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_studies_access.py +++ b/services/web/server/tests/unit/with_dbs/04/studies_dispatcher/test_studies_dispatcher_studies_access.py @@ -32,7 +32,7 @@ from servicelib.common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE from settings_library.utils_session import DEFAULT_SESSION_COOKIE_NAME from simcore_service_webserver.projects.models import ProjectDict -from simcore_service_webserver.projects.projects_api import submit_delete_project_task +from simcore_service_webserver.projects.projects_service import submit_delete_project_task from simcore_service_webserver.users.api import ( delete_user_without_projects, get_user_role, @@ -134,7 +134,7 @@ def mocks_on_projects_api(mocker: MockerFixture) -> None: All projects in this module are UNLOCKED """ mocker.patch( - "simcore_service_webserver.projects.projects_api._get_project_lock_state", + "simcore_service_webserver.projects.projects_service._get_project_lock_state", return_value=ProjectLocked(value=False, status=ProjectStatus.CLOSED), ) 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 d8053ab8264..f7dd5a8e1aa 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 @@ -259,7 +259,7 @@ def mock_storage_delete_data_folders(mocker: MockerFixture) -> mock.Mock: autospec=True, ) mocker.patch( - "simcore_service_webserver.projects.projects_api.remove_project_dynamic_services", + "simcore_service_webserver.projects.projects_service.remove_project_dynamic_services", autospec=True, ) mocker.patch( diff --git a/services/web/server/tests/unit/with_dbs/conftest.py b/services/web/server/tests/unit/with_dbs/conftest.py index 6de000d9686..f2556f81afe 100644 --- a/services/web/server/tests/unit/with_dbs/conftest.py +++ b/services/web/server/tests/unit/with_dbs/conftest.py @@ -383,7 +383,7 @@ async def _mock_result(): ) mock2 = mocker.patch( - "simcore_service_webserver.projects.projects_api.storage_api.delete_data_folders_of_project_node", + "simcore_service_webserver.projects.projects_service.storage_api.delete_data_folders_of_project_node", autospec=True, return_value=None, )