Skip to content

Commit aef71a1

Browse files
committed
@sanderegg review: decouple model adapters
1 parent 830a57a commit aef71a1

File tree

4 files changed

+132
-78
lines changed

4 files changed

+132
-78
lines changed

packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,6 @@ class ProjectNodeCreate(BaseModel):
7171
def get_field_names(cls, *, exclude: set[str]) -> set[str]:
7272
return cls.model_fields.keys() - exclude
7373

74-
def model_dump_as_node(self) -> dict[str, Any]:
75-
"""Converts a ProjectNode from the database to a Node model for the API.
76-
77-
Handles field mapping and excludes database-specific fields that are not
78-
part of the Node model.
79-
80-
NOTE: tested in services/web/server/tests/unit/isolated/test_models.py
81-
"""
82-
# Get all ProjectNode fields except those that don't belong in Node
83-
exclude_fields = {"node_id", "required_resources"}
84-
return self.model_dump(
85-
# NOTE: this setup ensures using the defaults provided in Node model when the db does not
86-
# provide them, e.g. `state`
87-
exclude=exclude_fields,
88-
exclude_none=True,
89-
exclude_unset=True,
90-
)
91-
9274
model_config = ConfigDict(frozen=True)
9375

9476

@@ -98,24 +80,6 @@ class ProjectNode(ProjectNodeCreate):
9880

9981
model_config = ConfigDict(from_attributes=True)
10082

101-
def model_dump_as_node(self) -> dict[str, Any]:
102-
"""Converts a ProjectNode from the database to a Node model for the API.
103-
104-
Handles field mapping and excludes database-specific fields that are not
105-
part of the Node model.
106-
107-
NOTE: tested in services/web/server/tests/unit/isolated/test_models.py
108-
"""
109-
# Get all ProjectNode fields except those that don't belong in Node
110-
exclude_fields = {"node_id", "required_resources", "created", "modified"}
111-
return self.model_dump(
112-
# NOTE: this setup ensures using the defaults provided in Node model when the db does not
113-
# provide them, e.g. `state`
114-
exclude=exclude_fields,
115-
exclude_none=True,
116-
exclude_unset=True,
117-
)
118-
11983

12084
@dataclass(frozen=True, kw_only=True)
12185
class ProjectNodesRepo:
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Collection of free function adapters between Node-like pydantic models
3+
4+
- The tricky part here is to deal with alias in Node model which are not present in the DB models
5+
6+
"""
7+
8+
from datetime import datetime
9+
from typing import Any
10+
from uuid import UUID
11+
12+
from models_library.projects_nodes import Node
13+
from simcore_postgres_database.utils_projects_nodes import (
14+
ProjectNode,
15+
ProjectNodeCreate,
16+
)
17+
18+
19+
def node_from_project_node_create(project_node_create: ProjectNodeCreate) -> Node:
20+
"""
21+
Adapter: Converts a ProjectNodeCreate instance to a Node model.
22+
"""
23+
exclude_fields = {"node_id", "required_resources"}
24+
25+
assert set(ProjectNodeCreate.model_fields).issuperset(exclude_fields) # nosec
26+
27+
node_data: dict[str, Any] = project_node_create.model_dump(
28+
exclude=exclude_fields,
29+
exclude_none=True,
30+
exclude_unset=True,
31+
)
32+
return Node.model_validate(node_data, by_name=True)
33+
34+
35+
def node_from_project_node(project_node: ProjectNode) -> Node:
36+
"""
37+
Adapter: Converts a ProjectNode instance to a Node model.
38+
"""
39+
exclude_fields = {"node_id", "required_resources", "created", "modified"}
40+
assert set(ProjectNode.model_fields).issuperset(exclude_fields) # nosec
41+
42+
node_data: dict[str, Any] = project_node.model_dump(
43+
exclude=exclude_fields,
44+
exclude_none=True,
45+
exclude_unset=True,
46+
)
47+
return Node.model_validate(node_data, by_name=True)
48+
49+
50+
def project_node_create_from_node(node: Node, node_id: UUID) -> ProjectNodeCreate:
51+
"""
52+
Adapter: Converts a Node model and node_id to a ProjectNodeCreate instance.
53+
"""
54+
node_data: dict[str, Any] = node.model_dump(by_alias=False, mode="json")
55+
return ProjectNodeCreate(node_id=node_id, **node_data)
56+
57+
58+
def project_node_from_node(
59+
node: Node, node_id: UUID, created: datetime, modified: datetime
60+
) -> ProjectNode:
61+
"""
62+
Adapter: Converts a Node model, node_id, created, and modified to a ProjectNode instance.
63+
"""
64+
node_data: dict[str, Any] = node.model_dump(by_alias=False, mode="json")
65+
return ProjectNode(
66+
node_id=node_id,
67+
created=created,
68+
modified=modified,
69+
**node_data,
70+
)

services/web/server/tests/unit/isolated/test_models.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,8 @@
77

88
import pytest
99
from faker import Faker
10-
from models_library.projects_nodes import Node
1110
from pydantic import TypeAdapter, ValidationError
1211
from pytest_simcore.helpers.faker_factories import random_phone_number
13-
from simcore_postgres_database.utils_projects_nodes import (
14-
ProjectNode,
15-
ProjectNodeCreate,
16-
)
1712
from simcore_service_webserver.users._controller.rest._rest_schemas import (
1813
MyPhoneRegister,
1914
PhoneNumberStr,
@@ -72,40 +67,3 @@ def test_invalid_phone_numbers(phone: str):
7267
# This test is used to tune options of PhoneNumberValidator
7368
with pytest.raises(ValidationError):
7469
MyPhoneRegister.model_validate({"phone": phone})
75-
76-
77-
_node_domain_model_dict_examples = Node.model_json_schema()["examples"]
78-
79-
80-
@pytest.mark.parametrize(
81-
"node_data",
82-
_node_domain_model_dict_examples,
83-
ids=[f"example-{i}" for i in range(len(_node_domain_model_dict_examples))],
84-
)
85-
def test_adapters_between_different_node_models(node_data: dict, faker: Faker):
86-
"""
87-
NOTE: This test is here because it checks models from models_library and simcore_postgres_database
88-
which are in different packages and should not depend on each other.
89-
"""
90-
# dict -> to Node (from models_library)
91-
node_id = faker.uuid4()
92-
node = Node.model_validate(node_data)
93-
94-
# -> to ProjectNodeCreate and ProjectNode (from simcore_postgres_database)
95-
project_node_create = ProjectNodeCreate(
96-
node_id=node_id,
97-
**node.model_dump(by_alias=False, mode="json"),
98-
)
99-
project_node = ProjectNode(
100-
node_id=node_id,
101-
created=faker.date_time(),
102-
modified=faker.date_time(),
103-
**node.model_dump(by_alias=False, mode="json"),
104-
)
105-
106-
# -> to Node (from models_library)
107-
assert (
108-
Node.model_validate(project_node_create.model_dump_as_node(), by_name=True)
109-
== node
110-
)
111-
assert Node.model_validate(project_node.model_dump_as_node(), by_name=True) == node
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# pylint: disable=protected-access
2+
# pylint: disable=redefined-outer-name
3+
# pylint: disable=too-many-arguments
4+
# pylint: disable=unused-argument
5+
# pylint: disable=unused-variable
6+
7+
8+
from typing import Any
9+
from uuid import UUID
10+
11+
import pytest
12+
from faker import Faker
13+
from models_library.projects_nodes import Node
14+
from simcore_postgres_database.utils_projects_nodes import (
15+
ProjectNode,
16+
ProjectNodeCreate,
17+
)
18+
from simcore_service_webserver.projects import _nodes_models_adapters
19+
20+
_NODE_DOMAIN_MODEL_DICT_EXAMPLES = Node.model_json_schema()["examples"]
21+
22+
23+
@pytest.mark.parametrize(
24+
"node_data",
25+
_NODE_DOMAIN_MODEL_DICT_EXAMPLES,
26+
ids=[f"example-{i}" for i in range(len(_NODE_DOMAIN_MODEL_DICT_EXAMPLES))],
27+
)
28+
def test_adapters_between_different_node_models(
29+
node_data: dict[str, Any], faker: Faker
30+
):
31+
# dict -> to Node (from models_library)
32+
node_id = UUID(faker.uuid4())
33+
node = Node.model_validate(node_data)
34+
35+
# Node -> ProjectNodeCreate (from simcore_postgres_database) using adapters
36+
project_node_create = _nodes_models_adapters.project_node_create_from_node(
37+
node, node_id
38+
)
39+
assert isinstance(project_node_create, ProjectNodeCreate)
40+
assert project_node_create.node_id == node_id
41+
42+
# Node -> ProjectNode (from simcore_postgres_database) using adapters
43+
project_node = _nodes_models_adapters.project_node_from_node(
44+
node,
45+
node_id,
46+
created=faker.date_time(),
47+
modified=faker.date_time(),
48+
)
49+
50+
assert isinstance(project_node, ProjectNode)
51+
assert project_node.node_id == node_id
52+
assert project_node.created != project_node.modified
53+
assert project_node_create.node_id == node_id
54+
55+
# ProjectNodeCreate -> Node (from models_library) using adapters
56+
assert (
57+
_nodes_models_adapters.node_from_project_node_create(project_node_create)
58+
== node
59+
)
60+
61+
# ProjectNode -> Node (from models_library) using adapters
62+
assert _nodes_models_adapters.node_from_project_node(project_node) == node

0 commit comments

Comments
 (0)