From f769e4d818e763443849fe7a3d26cd76b6830a38 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:53:44 +0200 Subject: [PATCH 01/12] changes in modeling --- .../api_schemas_webserver/projects.py | 8 +--- .../src/models_library/projects.py | 14 +++---- .../src/models_library/projects_nodes.py | 38 ++++++++++++++----- .../src/models_library/projects_nodes_io.py | 3 +- .../tests/test_services_types.py | 19 +++++++++- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/packages/models-library/src/models_library/api_schemas_webserver/projects.py b/packages/models-library/src/models_library/api_schemas_webserver/projects.py index 6ef501c23114..083628693882 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/projects.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/projects.py @@ -126,7 +126,7 @@ class ProjectGet(OutputSchema): # display name: str - description: str + description: Annotated[str, BeforeValidator(none_to_empty_str_pre_validator)] thumbnail: HttpUrl | Literal[""] type: ProjectType @@ -144,7 +144,7 @@ class ProjectGet(OutputSchema): trashed_at: datetime | None trashed_by: Annotated[ GroupID | None, Field(description="The primary gid of the user who trashed") - ] + ] = None # labeling tags: list[int] @@ -166,10 +166,6 @@ class ProjectGet(OutputSchema): workspace_id: WorkspaceID | None folder_id: FolderID | None - _empty_description = field_validator("description", mode="before")( - none_to_empty_str_pre_validator - ) - @staticmethod def _update_json_schema_extra(schema: JsonDict) -> None: schema.update( diff --git a/packages/models-library/src/models_library/projects.py b/packages/models-library/src/models_library/projects.py index 2092d1fce936..e66c5a54543f 100644 --- a/packages/models-library/src/models_library/projects.py +++ b/packages/models-library/src/models_library/projects.py @@ -10,6 +10,7 @@ from common_library.basic_types import DEFAULT_FACTORY from pydantic import ( BaseModel, + BeforeValidator, ConfigDict, Field, HttpUrl, @@ -85,6 +86,7 @@ class BaseProjectModel(BaseModel): ] description: Annotated[ str, + BeforeValidator(none_to_empty_str_pre_validator), Field( description="longer one-line description about the project", examples=["Dabbling in temporal transitions ..."], @@ -92,6 +94,9 @@ class BaseProjectModel(BaseModel): ] thumbnail: Annotated[ HttpUrl | None, + BeforeValidator( + empty_str_to_none_pre_validator, + ), Field( description="url of the project thumbnail", examples=["https://placeimg.com/171/96/tech/grayscale/?0.jpg"], @@ -104,15 +109,6 @@ class BaseProjectModel(BaseModel): # Pipeline of nodes (SEE projects_nodes.py) workbench: Annotated[NodesDict, Field(description="Project's pipeline")] - # validators - _empty_thumbnail_is_none = field_validator("thumbnail", mode="before")( - empty_str_to_none_pre_validator - ) - - _none_description_is_empty = field_validator("description", mode="before")( - none_to_empty_str_pre_validator - ) - class ProjectAtDB(BaseProjectModel): # Model used to READ from database diff --git a/packages/models-library/src/models_library/projects_nodes.py b/packages/models-library/src/models_library/projects_nodes.py index 24d2e9a79915..4f9e055605d6 100644 --- a/packages/models-library/src/models_library/projects_nodes.py +++ b/packages/models-library/src/models_library/projects_nodes.py @@ -177,14 +177,22 @@ class NodeState(BaseModel): model_config = ConfigDict( extra="forbid", - populate_by_name=True, + validate_by_alias=True, + validate_by_name=True, json_schema_extra={ "examples": [ + # example with alias name { "modified": True, "dependencies": [], "currentStatus": "NOT_STARTED", }, + # example with field name + { + "modified": True, + "dependencies": [], + "current_status": "NOT_STARTED", + }, { "modified": True, "dependencies": ["42838344-03de-4ce2-8d93-589a5dcdfd05"], @@ -230,7 +238,7 @@ class Node(BaseModel): Field(description="The short name of the node", examples=["JupyterLab"]), ] progress: Annotated[ - float | None, + int | None, Field( ge=0, le=100, @@ -302,15 +310,20 @@ class Node(BaseModel): Field(default_factory=dict, description="values of output properties"), ] = DEFAULT_FACTORY - output_node: Annotated[bool | None, Field(deprecated=True, alias="outputNode")] = ( - None # <-- (DEPRECATED) Can be removed - ) + output_node: Annotated[ + bool | None, + Field( + deprecated=True, + alias="outputNode", + ), + ] = None # <-- (DEPRECATED) Can be removed output_nodes: Annotated[ # <-- (DEPRECATED) Can be removed list[NodeID] | None, Field( description="Used in group-nodes. Node IDs of those connected to the output", alias="outputNodes", + deprecated=True, ), ] = None @@ -318,6 +331,7 @@ class Node(BaseModel): NodeID | None, Field( description="Parent's (group-nodes') node ID s. Used to group", + deprecated=True, ), ] = None @@ -334,6 +348,10 @@ class Node(BaseModel): Field(default_factory=NodeState, description="The node's state object"), ] = DEFAULT_FACTORY + required_resources: Annotated[ + dict[str, Any] | None, Field(default_factory=dict) + ] = DEFAULT_FACTORY + boot_options: Annotated[ dict[EnvVarKey, str] | None, Field( @@ -453,12 +471,14 @@ def _update_json_schema_extra(schema: JsonDict) -> None: model_config = ConfigDict( extra="forbid", - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, json_schema_extra=_update_json_schema_extra, ) class PartialNode(Node): - key: Annotated[ServiceKey, Field(default=None)] - version: Annotated[ServiceVersion, Field(default=None)] - label: Annotated[str, Field(default=None)] + # NOTE: `type: ignore[assignment]` is needed because mypy gets confused when overriding the types by adding the Union with None + key: ServiceKey | None = None # type: ignore[assignment] + version: ServiceVersion | None = None # type: ignore[assignment] + label: str | None = None # type: ignore[assignment] diff --git a/packages/models-library/src/models_library/projects_nodes_io.py b/packages/models-library/src/models_library/projects_nodes_io.py index 90fdf1412780..d4708582dead 100644 --- a/packages/models-library/src/models_library/projects_nodes_io.py +++ b/packages/models-library/src/models_library/projects_nodes_io.py @@ -30,10 +30,9 @@ UUID_RE, ) -NodeID = UUID - UUIDStr: TypeAlias = Annotated[str, StringConstraints(pattern=UUID_RE)] +NodeID: TypeAlias = UUID NodeIDStr: TypeAlias = UUIDStr LocationID: TypeAlias = int diff --git a/packages/models-library/tests/test_services_types.py b/packages/models-library/tests/test_services_types.py index 206c531a78fd..e3f0c9d472b2 100644 --- a/packages/models-library/tests/test_services_types.py +++ b/packages/models-library/tests/test_services_types.py @@ -1,9 +1,13 @@ import pytest from models_library.projects import ProjectID from models_library.projects_nodes import NodeID -from models_library.services_types import ServiceRunID +from models_library.services_types import ServiceKey, ServiceRunID, ServiceVersion from models_library.users import UserID -from pydantic import PositiveInt +from pydantic import PositiveInt, TypeAdapter +from pytest_simcore.helpers.faker_factories import ( + random_service_key, + random_service_version, +) @pytest.mark.parametrize( @@ -38,3 +42,14 @@ def test_get_resource_tracking_run_id_for_dynamic(): assert isinstance( ServiceRunID.get_resource_tracking_run_id_for_dynamic(), ServiceRunID ) + + +@pytest.mark.parametrize( + "service_key, service_version", + [(random_service_key(), random_service_version()) for _ in range(10)], +) +def test_faker_factory_service_key_and_version_are_in_sync( + service_key: ServiceKey, service_version: ServiceVersion +): + TypeAdapter(ServiceKey).validate_python(service_key) + TypeAdapter(ServiceVersion).validate_python(service_version) From 71458eceb244a940838ed4bb084268ee16d7ee55 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:59:11 +0200 Subject: [PATCH 02/12] changes in postgres package utils --- .../utils_projects.py | 19 +++++- .../utils_projects_nodes.py | 58 +++++++++++++++---- .../tests/test_models_projects_to_jobs.py | 36 +++++++++++- 3 files changed, 98 insertions(+), 15 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_projects.py b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py index 577f9441004b..ee6a8a132e89 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py @@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncConnection from .models.projects import projects -from .utils_repos import transaction_context +from .utils_repos import pass_or_acquire_connection, transaction_context class DBBaseProjectError(OsparcErrorMixin, Exception): @@ -22,6 +22,23 @@ class ProjectsRepo: def __init__(self, engine): self.engine = engine + async def exists( + self, + project_uuid: uuid.UUID, + *, + connection: AsyncConnection | None = None, + ) -> bool: + async with pass_or_acquire_connection(self.engine, connection) as conn: + return ( + await conn.scalar( + sa.select(1) + .select_from(projects) + .where(projects.c.uuid == f"{project_uuid}") + .limit(1) + ) + is not None + ) + async def get_project_last_change_date( self, project_uuid: uuid.UUID, 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 f82481939520..171967fe7b3b 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 @@ -4,19 +4,18 @@ from typing import Annotated, Any import asyncpg.exceptions # type: ignore[import-untyped] -import sqlalchemy import sqlalchemy.exc from common_library.async_tools import maybe_await from common_library.basic_types import DEFAULT_FACTORY from common_library.errors_classes import OsparcErrorMixin from pydantic import BaseModel, ConfigDict, Field -from simcore_postgres_database.utils_aiosqlalchemy import map_db_exception from sqlalchemy.dialects.postgresql import insert as pg_insert from ._protocols import DBConnection from .aiopg_errors import ForeignKeyViolation, UniqueViolation from .models.projects_node_to_pricing_unit import projects_node_to_pricing_unit from .models.projects_nodes import projects_nodes +from .utils_aiosqlalchemy import map_db_exception # @@ -59,6 +58,7 @@ class ProjectNodeCreate(BaseModel): input_access: dict[str, Any] | None = None input_nodes: list[str] | None = None inputs: dict[str, Any] | None = None + inputs_required: list[str] | None = None inputs_units: dict[str, Any] | None = None output_nodes: list[str] | None = None outputs: dict[str, Any] | None = None @@ -71,6 +71,22 @@ class ProjectNodeCreate(BaseModel): def get_field_names(cls, *, exclude: set[str]) -> set[str]: return cls.model_fields.keys() - exclude + def model_dump_as_node(self) -> dict[str, Any]: + """Converts a ProjectNode from the database to a Node model for the API. + + Handles field mapping and excludes database-specific fields that are not + part of the Node model. + """ + # Get all ProjectNode fields except those that don't belong in Node + exclude_fields = {"node_id", "required_resources"} + return self.model_dump( + # NOTE: this setup ensures using the defaults provided in Node model when the db does not + # provide them, e.g. `state` + exclude=exclude_fields, + exclude_none=True, + exclude_unset=True, + ) + model_config = ConfigDict(frozen=True) @@ -80,6 +96,22 @@ class ProjectNode(ProjectNodeCreate): model_config = ConfigDict(from_attributes=True) + def model_dump_as_node(self) -> dict[str, Any]: + """Converts a ProjectNode from the database to a Node model for the API. + + Handles field mapping and excludes database-specific fields that are not + part of the Node model. + """ + # Get all ProjectNode fields except those that don't belong in Node + exclude_fields = {"node_id", "required_resources", "created", "modified"} + return self.model_dump( + # NOTE: this setup ensures using the defaults provided in Node model when the db does not + # provide them, e.g. `state` + exclude=exclude_fields, + exclude_none=True, + exclude_unset=True, + ) + @dataclass(frozen=True, kw_only=True) class ProjectNodesRepo: @@ -103,17 +135,18 @@ async def add( """ if not nodes: return [] + + values = [ + { + "project_uuid": f"{self.project_uuid}", + **node.model_dump(mode="json"), + } + for node in nodes + ] + insert_stmt = ( projects_nodes.insert() - .values( - [ - { - "project_uuid": f"{self.project_uuid}", - **node.model_dump(exclude_unset=True, mode="json"), - } - for node in nodes - ] - ) + .values(values) .returning( *[ c @@ -129,14 +162,17 @@ async def add( rows = await maybe_await(result.fetchall()) assert isinstance(rows, list) # nosec return [ProjectNode.model_validate(r) for r in rows] + except ForeignKeyViolation as exc: # this happens when the project does not exist, as we first check the node exists raise ProjectNodesProjectNotFoundError( project_uuid=self.project_uuid ) from exc + except UniqueViolation as exc: # this happens if the node already exists on creation raise ProjectNodesDuplicateNodeError from exc + except sqlalchemy.exc.IntegrityError as exc: raise map_db_exception( exc, diff --git a/packages/postgres-database/tests/test_models_projects_to_jobs.py b/packages/postgres-database/tests/test_models_projects_to_jobs.py index e2e5cf0476e2..a7db2a0623ec 100644 --- a/packages/postgres-database/tests/test_models_projects_to_jobs.py +++ b/packages/postgres-database/tests/test_models_projects_to_jobs.py @@ -3,6 +3,7 @@ # pylint: disable=unused-variable # pylint: disable=too-many-arguments +import json from collections.abc import Iterator import pytest @@ -14,7 +15,7 @@ from faker import Faker from pytest_simcore.helpers import postgres_tools from pytest_simcore.helpers.faker_factories import random_project, random_user -from simcore_postgres_database.models.projects import projects +from simcore_postgres_database.models.projects import ProjectType, projects from simcore_postgres_database.models.projects_to_jobs import projects_to_jobs @@ -97,7 +98,7 @@ def test_populate_projects_to_jobs_during_migration( "Study associated to solver job:" """{ "id": "cd03450c-4c17-4c2c-85fd-0d951d7dcd5a", - "name": "solvers/simcore%2Fservices%2Fcomp%2Fitis%2Fsleeper/releases/2.2.1/jobs/cd03450c-4c2c-85fd-0d951d7dcd5a", + "name": "solvers/simcore%2Fservices%2Fcomp%2Fitis%2Fsleeper/releases/2.2.1/jobs/cd03450c-4c17-4c2c-85fd-0d951d7dcd5a", "inputs_checksum": "015ba4cd5cf00c511a8217deb65c242e3b15dc6ae4b1ecf94982d693887d9e8a", "created_at": "2025-01-27T13:12:58.676564Z" } @@ -120,8 +121,37 @@ def test_populate_projects_to_jobs_during_migration( prj_owner=user_id, ), ] + + default_column_values = { + # NOTE: not server_default values are not applied here! + "type": ProjectType.STANDARD.value, + "workbench": {}, + "access_rights": {}, + "published": False, + "hidden": False, + "workspace_id": None, + } + + # NOTE: cannot use `projects` table directly here because it changes + # throughout time for prj in projects_data: - conn.execute(sa.insert(projects).values(prj)) + for key, value in default_column_values.items(): + prj.setdefault(key, value) + + for key, value in prj.items(): + if isinstance(value, dict): + prj[key] = json.dumps(value) + + columns = list(prj.keys()) + values_clause = ", ".join(f":{col}" for col in columns) + columns_clause = ", ".join(columns) + stmt = sa.text( + f""" + INSERT INTO projects ({columns_clause}) + VALUES ({values_clause}) + """ # noqa: S608 + ).bindparams(**prj) + conn.execute(stmt) # MIGRATE UPGRADE: this should populate simcore_postgres_database.cli.upgrade.callback("head") From bdda03b1e238dfe531436be906121bca1b2e05bb Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:02:46 +0200 Subject: [PATCH 03/12] ne fakes --- .../pytest_simcore/helpers/faker_factories.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py b/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py index ef006f53105b..39bc718307d8 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py @@ -25,6 +25,15 @@ DEFAULT_FAKER: Final = Faker() +def random_service_key(fake: Faker = DEFAULT_FAKER, *, name: str | None = None) -> str: + suffix = fake.unique.word() if name is None else name + return f"simcore/services/{fake.random_element(['dynamic', 'comp', 'frontend'])}/{suffix.lower()}" + + +def random_service_version(fake: Faker = DEFAULT_FAKER) -> str: + return ".".join([str(fake.pyint(0, 100)) for _ in range(3)]) + + def random_icon_url(fake: Faker): return fake.image_url(width=16, height=16) @@ -173,7 +182,6 @@ def random_project(fake: Faker = DEFAULT_FAKER, **overrides) -> dict[str, Any]: "prj_owner": fake.pyint(), "thumbnail": fake.image_url(width=120, height=120), "access_rights": {}, - "workbench": {}, "published": False, } @@ -187,6 +195,26 @@ def random_project(fake: Faker = DEFAULT_FAKER, **overrides) -> dict[str, Any]: return data +def random_project_node(fake: Faker = DEFAULT_FAKER, **overrides) -> dict[str, Any]: + """Generates random fake data project nodes DATABASE table""" + from simcore_postgres_database.models.projects_nodes import projects_nodes + + _name = fake.name() + + data = { + "node_id": fake.uuid4(), + "project_uuid": fake.uuid4(), + "key": random_service_key(fake, name=_name), + "version": random_service_version(fake), + "label": _name, + } + + assert set(data.keys()).issubset({c.name for c in projects_nodes.columns}) + + data.update(overrides) + return data + + def random_group(fake: Faker = DEFAULT_FAKER, **overrides) -> dict[str, Any]: from simcore_postgres_database.models.groups import groups from simcore_postgres_database.webserver_models import GroupType @@ -483,7 +511,7 @@ def random_service_meta_data( ) -> dict[str, Any]: from simcore_postgres_database.models.services import services_meta_data - _version = ".".join([str(fake.pyint()) for _ in range(3)]) + _version = random_service_version(fake) _name = fake.name() data: dict[str, Any] = { From 2c3c4d1d53006bc5a3f22a3f2760cb99606aed14 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:03:19 +0200 Subject: [PATCH 04/12] new asserts --- .../pytest_simcore/helpers/assert_checks.py | 14 ++++++++ .../tests/test_helpers_asserts_checks.py | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 packages/pytest-simcore/tests/test_helpers_asserts_checks.py diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py b/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py index fc931cbebd59..afc6cdf15a3f 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py @@ -97,3 +97,17 @@ def _do_assert_error( assert expected_error_code in codes return data, error + + +def assert_equal_ignoring_none(expected: dict, actual: dict): + for key, exp_value in expected.items(): + if exp_value is None: + continue + assert key in actual, f"Missing key {key}" + act_value = actual[key] + if isinstance(exp_value, dict) and isinstance(act_value, dict): + assert_equal_ignoring_none(exp_value, act_value) + else: + assert ( + act_value == exp_value + ), f"Mismatch in {key}: {act_value} != {exp_value}" diff --git a/packages/pytest-simcore/tests/test_helpers_asserts_checks.py b/packages/pytest-simcore/tests/test_helpers_asserts_checks.py new file mode 100644 index 000000000000..189d84f9f81e --- /dev/null +++ b/packages/pytest-simcore/tests/test_helpers_asserts_checks.py @@ -0,0 +1,35 @@ +import pytest +from pytest_simcore.helpers.assert_checks import assert_equal_ignoring_none + + +@pytest.mark.parametrize( + "expected, actual", + [ + ({"a": 1, "b": 2}, {"a": 1, "b": 2, "c": 3}), + ({"a": 1, "b": None}, {"a": 1, "b": 42}), + ({"a": {"x": 10, "y": None}}, {"a": {"x": 10, "y": 99}}), + ({"a": {"x": 10, "y": 20}}, {"a": {"x": 10, "y": 20, "z": 30}}), + ({}, {"foo": "bar"}), + ], +) +def test_assert_equal_ignoring_none_passes(expected, actual): + assert_equal_ignoring_none(expected, actual) + + +@pytest.mark.parametrize( + "expected, actual, error_msg", + [ + ({"a": 1, "b": 2}, {"a": 1}, "Missing key b"), + ({"a": 1, "b": 2}, {"a": 1, "b": 3}, "Mismatch in b: 3 != 2"), + ( + {"a": {"x": 10, "y": 20}}, + {"a": {"x": 10, "y": 99}}, + "Mismatch in y: 99 != 20", + ), + ({"a": {"x": 10}}, {"a": {}}, "Missing key x"), + ], +) +def test_assert_equal_ignoring_none_fails(expected, actual, error_msg): + with pytest.raises(AssertionError) as exc_info: + assert_equal_ignoring_none(expected, actual) + assert error_msg in str(exc_info.value) From d9e0bb0b99f6a1eb05e66f9f25b6d13f97307799 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:12:43 +0200 Subject: [PATCH 05/12] test nodes --- .../utils_projects_nodes.py | 4 ++ .../server/tests/unit/isolated/test_models.py | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+) 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 171967fe7b3b..e29d7ae3fb7a 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 @@ -76,6 +76,8 @@ def model_dump_as_node(self) -> dict[str, Any]: Handles field mapping and excludes database-specific fields that are not part of the Node model. + + NOTE: tested in services/web/server/tests/unit/isolated/test_models.py """ # Get all ProjectNode fields except those that don't belong in Node exclude_fields = {"node_id", "required_resources"} @@ -101,6 +103,8 @@ def model_dump_as_node(self) -> dict[str, Any]: Handles field mapping and excludes database-specific fields that are not part of the Node model. + + NOTE: tested in services/web/server/tests/unit/isolated/test_models.py """ # Get all ProjectNode fields except those that don't belong in Node exclude_fields = {"node_id", "required_resources", "created", "modified"} diff --git a/services/web/server/tests/unit/isolated/test_models.py b/services/web/server/tests/unit/isolated/test_models.py index 04e22afc6843..b4f801267112 100644 --- a/services/web/server/tests/unit/isolated/test_models.py +++ b/services/web/server/tests/unit/isolated/test_models.py @@ -7,8 +7,13 @@ import pytest from faker import Faker +from models_library.projects_nodes import Node from pydantic import TypeAdapter, ValidationError from pytest_simcore.helpers.faker_factories import random_phone_number +from simcore_postgres_database.utils_projects_nodes import ( + ProjectNode, + ProjectNodeCreate, +) from simcore_service_webserver.users._controller.rest._rest_schemas import ( MyPhoneRegister, PhoneNumberStr, @@ -67,3 +72,40 @@ def test_invalid_phone_numbers(phone: str): # This test is used to tune options of PhoneNumberValidator with pytest.raises(ValidationError): MyPhoneRegister.model_validate({"phone": phone}) + + +_node_domain_model_dict_examples = Node.model_json_schema()["examples"] + + +@pytest.mark.parametrize( + "node_data", + _node_domain_model_dict_examples, + ids=[f"example-{i}" for i in range(len(_node_domain_model_dict_examples))], +) +def test_adapters_between_different_node_models(node_data: dict, faker: Faker): + """ + NOTE: This test is here because it checks models from models_library and simcore_postgres_database + which are in different packages and should not depend on each other. + """ + # dict -> to Node (from models_library) + node_id = faker.uuid4() + node = Node.model_validate(node_data) + + # -> to ProjectNodeCreate and ProjectNode (from simcore_postgres_database) + project_node_create = ProjectNodeCreate( + node_id=node_id, + **node.model_dump(by_alias=False, mode="json"), + ) + project_node = ProjectNode( + node_id=node_id, + created=faker.date_time(), + modified=faker.date_time(), + **node.model_dump(by_alias=False, mode="json"), + ) + + # -> to Node (from models_library) + assert ( + Node.model_validate(project_node_create.model_dump_as_node(), by_name=True) + == node + ) + assert Node.model_validate(project_node.model_dump_as_node(), by_name=True) == node From 3376d31cca9cb201c9282248483df6c3b47eee07 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:13:56 +0200 Subject: [PATCH 06/12] miior --- .../api/v0/openapi.yaml | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index a52205fc60d9..3386f722727e 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 @@ -14006,9 +14006,9 @@ components: description: The short name of the node progress: anyOf: - - type: number - maximum: 100.0 - minimum: 0.0 + - type: integer + maximum: 100 + minimum: 0 - type: 'null' title: Progress description: the node progress value (deprecated in DB, still used for API @@ -14089,6 +14089,7 @@ components: - type: 'null' title: Outputnodes description: Used in group-nodes. Node IDs of those connected to the output + deprecated: true parent: anyOf: - type: string @@ -14096,6 +14097,7 @@ components: - type: 'null' title: Parent description: Parent's (group-nodes') node ID s. Used to group + deprecated: true position: anyOf: - $ref: '#/components/schemas/Position' @@ -14107,6 +14109,12 @@ components: - $ref: '#/components/schemas/NodeState-Input' - type: 'null' description: The node's state object + required_resources: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + title: Required Resources bootOptions: anyOf: - type: object @@ -14141,9 +14149,9 @@ components: description: The short name of the node progress: anyOf: - - type: number - maximum: 100.0 - minimum: 0.0 + - type: integer + maximum: 100 + minimum: 0 - type: 'null' title: Progress description: the node progress value (deprecated in DB, still used for API @@ -14224,6 +14232,7 @@ components: - type: 'null' title: Outputnodes description: Used in group-nodes. Node IDs of those connected to the output + deprecated: true parent: anyOf: - type: string @@ -14231,6 +14240,7 @@ components: - type: 'null' title: Parent description: Parent's (group-nodes') node ID s. Used to group + deprecated: true position: anyOf: - $ref: '#/components/schemas/Position' @@ -14242,6 +14252,12 @@ components: - $ref: '#/components/schemas/NodeState-Output' - type: 'null' description: The node's state object + required_resources: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + title: Required Resources bootOptions: anyOf: - type: object @@ -15953,7 +15969,6 @@ components: - creationDate - lastChangeDate - trashedAt - - trashedBy - tags - dev - workspaceId @@ -16156,7 +16171,6 @@ components: - creationDate - lastChangeDate - trashedAt - - trashedBy - tags - dev - workspaceId From 499126c941f6c0e7e01b145aaeb5fb137cc26442 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:34:27 +0200 Subject: [PATCH 07/12] fix fake --- .../src/pytest_simcore/helpers/faker_factories.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py b/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py index 39bc718307d8..405753670914 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/faker_factories.py @@ -182,6 +182,7 @@ def random_project(fake: Faker = DEFAULT_FAKER, **overrides) -> dict[str, Any]: "prj_owner": fake.pyint(), "thumbnail": fake.image_url(width=120, height=120), "access_rights": {}, + "workbench": {}, "published": False, } @@ -199,14 +200,14 @@ def random_project_node(fake: Faker = DEFAULT_FAKER, **overrides) -> dict[str, A """Generates random fake data project nodes DATABASE table""" from simcore_postgres_database.models.projects_nodes import projects_nodes - _name = fake.name() + fake_name = fake.name() data = { "node_id": fake.uuid4(), "project_uuid": fake.uuid4(), - "key": random_service_key(fake, name=_name), + "key": random_service_key(fake, name=fake_name), "version": random_service_version(fake), - "label": _name, + "label": fake_name, } assert set(data.keys()).issubset({c.name for c in projects_nodes.columns}) From 69346bdf2edf5a67c595a7bfd1632d8f001da736 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:04:36 +0200 Subject: [PATCH 08/12] @GitHK review: fix wrong type and deprecation --- packages/aws-library/src/aws_library/s3/__init__.py | 6 +++--- .../src/models_library/projects_nodes.py | 13 +++++++------ .../models/domain/projects.py | 8 ++------ 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/aws-library/src/aws_library/s3/__init__.py b/packages/aws-library/src/aws_library/s3/__init__.py index ea8f6264d604..8a9a85f1279e 100644 --- a/packages/aws-library/src/aws_library/s3/__init__.py +++ b/packages/aws-library/src/aws_library/s3/__init__.py @@ -22,10 +22,10 @@ ) __all__: tuple[str, ...] = ( - "CopiedBytesTransferredCallback", - "MultiPartUploadLinks", "PRESIGNED_LINK_MAX_SIZE", "S3_MAX_FILE_SIZE", + "CopiedBytesTransferredCallback", + "MultiPartUploadLinks", "S3AccessError", "S3BucketInvalidError", "S3DestinationNotEmptyError", @@ -37,8 +37,8 @@ "S3RuntimeError", "S3UploadNotFoundError", "SimcoreS3API", - "UploadedBytesTransferredCallback", "UploadID", + "UploadedBytesTransferredCallback", ) # nopycln: file diff --git a/packages/models-library/src/models_library/projects_nodes.py b/packages/models-library/src/models_library/projects_nodes.py index 4f9e055605d6..f3460cce5cc0 100644 --- a/packages/models-library/src/models_library/projects_nodes.py +++ b/packages/models-library/src/models_library/projects_nodes.py @@ -238,20 +238,21 @@ class Node(BaseModel): Field(description="The short name of the node", examples=["JupyterLab"]), ] progress: Annotated[ - int | None, + float | None, Field( ge=0, le=100, - description="the node progress value (deprecated in DB, still used for API only)", - deprecated=True, # <-- Think this is not true, it is still used by the File Picker (frontend nodes) + description="the node progress value", + deprecated=True, # NOTE: still used in the File Picker (frontend nodes) and must be removed first from there before retiring it here ), ] = None - thumbnail: Annotated[ # <-- (DEPRECATED) Can be removed + thumbnail: Annotated[ str | HttpUrl | None, Field( description="url of the latest screenshot of the node", examples=["https://placeimg.com/171/96/tech/grayscale/?0.jpg"], + deprecated=True, ), ] = None @@ -316,7 +317,7 @@ class Node(BaseModel): deprecated=True, alias="outputNode", ), - ] = None # <-- (DEPRECATED) Can be removed + ] = None output_nodes: Annotated[ # <-- (DEPRECATED) Can be removed list[NodeID] | None, @@ -327,7 +328,7 @@ class Node(BaseModel): ), ] = None - parent: Annotated[ # <-- (DEPRECATED) Can be removed + parent: Annotated[ NodeID | None, Field( description="Parent's (group-nodes') node ID s. Used to group", diff --git a/services/api-server/src/simcore_service_api_server/models/domain/projects.py b/services/api-server/src/simcore_service_api_server/models/domain/projects.py index ae74533546b4..287bf985aa41 100644 --- a/services/api-server/src/simcore_service_api_server/models/domain/projects.py +++ b/services/api-server/src/simcore_service_api_server/models/domain/projects.py @@ -4,12 +4,6 @@ from models_library.projects_nodes import InputTypes, Node, OutputTypes from models_library.projects_nodes_io import SimCoreFileLink -assert AccessRights # nosec -assert InputTypes # nosec -assert Node # nosec -assert OutputTypes # nosec -assert SimCoreFileLink # nosec - __all__: tuple[str, ...] = ( "AccessRights", "InputTypes", @@ -17,3 +11,5 @@ "OutputTypes", "SimCoreFileLink", ) + +# nopycln: file From 830a57aacd165d7a9652639c5728fd9eda653cef Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:08:37 +0200 Subject: [PATCH 09/12] @sanderegg review: deprecation issue --- .../src/models_library/projects_nodes.py | 5 +++++ .../api/v0/openapi.yaml | 20 +++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/models-library/src/models_library/projects_nodes.py b/packages/models-library/src/models_library/projects_nodes.py index f3460cce5cc0..d0ff6bf22c94 100644 --- a/packages/models-library/src/models_library/projects_nodes.py +++ b/packages/models-library/src/models_library/projects_nodes.py @@ -244,6 +244,7 @@ class Node(BaseModel): le=100, description="the node progress value", deprecated=True, # NOTE: still used in the File Picker (frontend nodes) and must be removed first from there before retiring it here + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365 ), ] = None @@ -253,6 +254,7 @@ class Node(BaseModel): description="url of the latest screenshot of the node", examples=["https://placeimg.com/171/96/tech/grayscale/?0.jpg"], deprecated=True, + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365 ), ] = None @@ -316,6 +318,7 @@ class Node(BaseModel): Field( deprecated=True, alias="outputNode", + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365 ), ] = None @@ -325,6 +328,7 @@ class Node(BaseModel): description="Used in group-nodes. Node IDs of those connected to the output", alias="outputNodes", deprecated=True, + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365 ), ] = None @@ -333,6 +337,7 @@ class Node(BaseModel): Field( description="Parent's (group-nodes') node ID s. Used to group", deprecated=True, + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/8365 ), ] = None 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 ae09b1637338..d704cd3043a6 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 @@ -14006,13 +14006,12 @@ components: description: The short name of the node progress: anyOf: - - type: integer - maximum: 100 - minimum: 0 + - type: number + maximum: 100.0 + minimum: 0.0 - type: 'null' title: Progress - description: the node progress value (deprecated in DB, still used for API - only) + description: the node progress value deprecated: true thumbnail: anyOf: @@ -14024,6 +14023,7 @@ components: - type: 'null' title: Thumbnail description: url of the latest screenshot of the node + deprecated: true runHash: anyOf: - type: string @@ -14149,13 +14149,12 @@ components: description: The short name of the node progress: anyOf: - - type: integer - maximum: 100 - minimum: 0 + - type: number + maximum: 100.0 + minimum: 0.0 - type: 'null' title: Progress - description: the node progress value (deprecated in DB, still used for API - only) + description: the node progress value deprecated: true thumbnail: anyOf: @@ -14167,6 +14166,7 @@ components: - type: 'null' title: Thumbnail description: url of the latest screenshot of the node + deprecated: true runHash: anyOf: - type: string From aef71a17f48e47bf23b3b67a7e1b7488f61fcc29 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:46:44 +0200 Subject: [PATCH 10/12] @sanderegg review: decouple model adapters --- .../utils_projects_nodes.py | 36 ---------- .../projects/_nodes_models_adapters.py | 70 +++++++++++++++++++ .../server/tests/unit/isolated/test_models.py | 42 ----------- .../test_projects__nodes_models_adapters.py | 62 ++++++++++++++++ 4 files changed, 132 insertions(+), 78 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/projects/_nodes_models_adapters.py create mode 100644 services/web/server/tests/unit/isolated/test_projects__nodes_models_adapters.py 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 e29d7ae3fb7a..4bb6855b0bff 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 @@ -71,24 +71,6 @@ class ProjectNodeCreate(BaseModel): def get_field_names(cls, *, exclude: set[str]) -> set[str]: return cls.model_fields.keys() - exclude - def model_dump_as_node(self) -> dict[str, Any]: - """Converts a ProjectNode from the database to a Node model for the API. - - Handles field mapping and excludes database-specific fields that are not - part of the Node model. - - NOTE: tested in services/web/server/tests/unit/isolated/test_models.py - """ - # Get all ProjectNode fields except those that don't belong in Node - exclude_fields = {"node_id", "required_resources"} - return self.model_dump( - # NOTE: this setup ensures using the defaults provided in Node model when the db does not - # provide them, e.g. `state` - exclude=exclude_fields, - exclude_none=True, - exclude_unset=True, - ) - model_config = ConfigDict(frozen=True) @@ -98,24 +80,6 @@ class ProjectNode(ProjectNodeCreate): model_config = ConfigDict(from_attributes=True) - def model_dump_as_node(self) -> dict[str, Any]: - """Converts a ProjectNode from the database to a Node model for the API. - - Handles field mapping and excludes database-specific fields that are not - part of the Node model. - - NOTE: tested in services/web/server/tests/unit/isolated/test_models.py - """ - # Get all ProjectNode fields except those that don't belong in Node - exclude_fields = {"node_id", "required_resources", "created", "modified"} - return self.model_dump( - # NOTE: this setup ensures using the defaults provided in Node model when the db does not - # provide them, e.g. `state` - exclude=exclude_fields, - exclude_none=True, - exclude_unset=True, - ) - @dataclass(frozen=True, kw_only=True) class ProjectNodesRepo: diff --git a/services/web/server/src/simcore_service_webserver/projects/_nodes_models_adapters.py b/services/web/server/src/simcore_service_webserver/projects/_nodes_models_adapters.py new file mode 100644 index 000000000000..f5baf7b776cf --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/projects/_nodes_models_adapters.py @@ -0,0 +1,70 @@ +""" +Collection of free function adapters between Node-like pydantic models + +- The tricky part here is to deal with alias in Node model which are not present in the DB models + +""" + +from datetime import datetime +from typing import Any +from uuid import UUID + +from models_library.projects_nodes import Node +from simcore_postgres_database.utils_projects_nodes import ( + ProjectNode, + ProjectNodeCreate, +) + + +def node_from_project_node_create(project_node_create: ProjectNodeCreate) -> Node: + """ + Adapter: Converts a ProjectNodeCreate instance to a Node model. + """ + exclude_fields = {"node_id", "required_resources"} + + assert set(ProjectNodeCreate.model_fields).issuperset(exclude_fields) # nosec + + node_data: dict[str, Any] = project_node_create.model_dump( + exclude=exclude_fields, + exclude_none=True, + exclude_unset=True, + ) + return Node.model_validate(node_data, by_name=True) + + +def node_from_project_node(project_node: ProjectNode) -> Node: + """ + Adapter: Converts a ProjectNode instance to a Node model. + """ + exclude_fields = {"node_id", "required_resources", "created", "modified"} + assert set(ProjectNode.model_fields).issuperset(exclude_fields) # nosec + + node_data: dict[str, Any] = project_node.model_dump( + exclude=exclude_fields, + exclude_none=True, + exclude_unset=True, + ) + return Node.model_validate(node_data, by_name=True) + + +def project_node_create_from_node(node: Node, node_id: UUID) -> ProjectNodeCreate: + """ + Adapter: Converts a Node model and node_id to a ProjectNodeCreate instance. + """ + node_data: dict[str, Any] = node.model_dump(by_alias=False, mode="json") + return ProjectNodeCreate(node_id=node_id, **node_data) + + +def project_node_from_node( + node: Node, node_id: UUID, created: datetime, modified: datetime +) -> ProjectNode: + """ + Adapter: Converts a Node model, node_id, created, and modified to a ProjectNode instance. + """ + node_data: dict[str, Any] = node.model_dump(by_alias=False, mode="json") + return ProjectNode( + node_id=node_id, + created=created, + modified=modified, + **node_data, + ) diff --git a/services/web/server/tests/unit/isolated/test_models.py b/services/web/server/tests/unit/isolated/test_models.py index b4f801267112..04e22afc6843 100644 --- a/services/web/server/tests/unit/isolated/test_models.py +++ b/services/web/server/tests/unit/isolated/test_models.py @@ -7,13 +7,8 @@ import pytest from faker import Faker -from models_library.projects_nodes import Node from pydantic import TypeAdapter, ValidationError from pytest_simcore.helpers.faker_factories import random_phone_number -from simcore_postgres_database.utils_projects_nodes import ( - ProjectNode, - ProjectNodeCreate, -) from simcore_service_webserver.users._controller.rest._rest_schemas import ( MyPhoneRegister, PhoneNumberStr, @@ -72,40 +67,3 @@ def test_invalid_phone_numbers(phone: str): # This test is used to tune options of PhoneNumberValidator with pytest.raises(ValidationError): MyPhoneRegister.model_validate({"phone": phone}) - - -_node_domain_model_dict_examples = Node.model_json_schema()["examples"] - - -@pytest.mark.parametrize( - "node_data", - _node_domain_model_dict_examples, - ids=[f"example-{i}" for i in range(len(_node_domain_model_dict_examples))], -) -def test_adapters_between_different_node_models(node_data: dict, faker: Faker): - """ - NOTE: This test is here because it checks models from models_library and simcore_postgres_database - which are in different packages and should not depend on each other. - """ - # dict -> to Node (from models_library) - node_id = faker.uuid4() - node = Node.model_validate(node_data) - - # -> to ProjectNodeCreate and ProjectNode (from simcore_postgres_database) - project_node_create = ProjectNodeCreate( - node_id=node_id, - **node.model_dump(by_alias=False, mode="json"), - ) - project_node = ProjectNode( - node_id=node_id, - created=faker.date_time(), - modified=faker.date_time(), - **node.model_dump(by_alias=False, mode="json"), - ) - - # -> to Node (from models_library) - assert ( - Node.model_validate(project_node_create.model_dump_as_node(), by_name=True) - == node - ) - assert Node.model_validate(project_node.model_dump_as_node(), by_name=True) == node diff --git a/services/web/server/tests/unit/isolated/test_projects__nodes_models_adapters.py b/services/web/server/tests/unit/isolated/test_projects__nodes_models_adapters.py new file mode 100644 index 000000000000..af56734647cb --- /dev/null +++ b/services/web/server/tests/unit/isolated/test_projects__nodes_models_adapters.py @@ -0,0 +1,62 @@ +# pylint: disable=protected-access +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +# pylint: disable=unused-argument +# pylint: disable=unused-variable + + +from typing import Any +from uuid import UUID + +import pytest +from faker import Faker +from models_library.projects_nodes import Node +from simcore_postgres_database.utils_projects_nodes import ( + ProjectNode, + ProjectNodeCreate, +) +from simcore_service_webserver.projects import _nodes_models_adapters + +_NODE_DOMAIN_MODEL_DICT_EXAMPLES = Node.model_json_schema()["examples"] + + +@pytest.mark.parametrize( + "node_data", + _NODE_DOMAIN_MODEL_DICT_EXAMPLES, + ids=[f"example-{i}" for i in range(len(_NODE_DOMAIN_MODEL_DICT_EXAMPLES))], +) +def test_adapters_between_different_node_models( + node_data: dict[str, Any], faker: Faker +): + # dict -> to Node (from models_library) + node_id = UUID(faker.uuid4()) + node = Node.model_validate(node_data) + + # Node -> ProjectNodeCreate (from simcore_postgres_database) using adapters + project_node_create = _nodes_models_adapters.project_node_create_from_node( + node, node_id + ) + assert isinstance(project_node_create, ProjectNodeCreate) + assert project_node_create.node_id == node_id + + # Node -> ProjectNode (from simcore_postgres_database) using adapters + project_node = _nodes_models_adapters.project_node_from_node( + node, + node_id, + created=faker.date_time(), + modified=faker.date_time(), + ) + + assert isinstance(project_node, ProjectNode) + assert project_node.node_id == node_id + assert project_node.created != project_node.modified + assert project_node_create.node_id == node_id + + # ProjectNodeCreate -> Node (from models_library) using adapters + assert ( + _nodes_models_adapters.node_from_project_node_create(project_node_create) + == node + ) + + # ProjectNode -> Node (from models_library) using adapters + assert _nodes_models_adapters.node_from_project_node(project_node) == node From 2fe70ba1a0a38fd265a23c08ba4f71760361f740 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:51:19 +0200 Subject: [PATCH 11/12] @sanderegg review: doc --- .../postgres-database/tests/test_models_projects_to_jobs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/postgres-database/tests/test_models_projects_to_jobs.py b/packages/postgres-database/tests/test_models_projects_to_jobs.py index a7db2a0623ec..5b6436023d28 100644 --- a/packages/postgres-database/tests/test_models_projects_to_jobs.py +++ b/packages/postgres-database/tests/test_models_projects_to_jobs.py @@ -122,8 +122,8 @@ def test_populate_projects_to_jobs_during_migration( ), ] - default_column_values = { - # NOTE: not server_default values are not applied here! + client_default_column_values = { + # NOTE: columns with `server_default values` must not be added here "type": ProjectType.STANDARD.value, "workbench": {}, "access_rights": {}, @@ -135,7 +135,7 @@ def test_populate_projects_to_jobs_during_migration( # NOTE: cannot use `projects` table directly here because it changes # throughout time for prj in projects_data: - for key, value in default_column_values.items(): + for key, value in client_default_column_values.items(): prj.setdefault(key, value) for key, value in prj.items(): From c6903cd391809877a1de6d55534bb705ff6f88e5 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:36:51 +0200 Subject: [PATCH 12/12] @sanderegg review: drops required_resources --- packages/models-library/src/models_library/projects_nodes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/models-library/src/models_library/projects_nodes.py b/packages/models-library/src/models_library/projects_nodes.py index d0ff6bf22c94..1bb91c1ddaac 100644 --- a/packages/models-library/src/models_library/projects_nodes.py +++ b/packages/models-library/src/models_library/projects_nodes.py @@ -354,9 +354,8 @@ class Node(BaseModel): Field(default_factory=NodeState, description="The node's state object"), ] = DEFAULT_FACTORY - required_resources: Annotated[ - dict[str, Any] | None, Field(default_factory=dict) - ] = DEFAULT_FACTORY + # NOTE: requested_resources should be here! WARNING: this model is used both in database and rest api! + # Model for project_nodes table should NOT be Node but a different one ! boot_options: Annotated[ dict[EnvVarKey, str] | None,