Skip to content

Commit 49fe927

Browse files
committed
fix: robust synthetic property handling for Collections
- Refactored SavedCollection and CollectionWithUrn to ensure robust handling of synthetic and required fields for both ORM and dict contexts. - Added parameterized tests to verify all key attributes are correctly handled in both construction modes. - Added tests for creation from both dict and ORM contexts, mirroring the approach used for other models.
1 parent 656ad80 commit 49fe927

File tree

2 files changed

+137
-17
lines changed

2 files changed

+137
-17
lines changed

src/mavedb/view_models/collection.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import date
2-
from typing import Any, Sequence, Optional
2+
from typing import Any, Optional, Sequence
33

44
from pydantic import Field, model_validator
55

@@ -84,36 +84,36 @@ class Config:
8484
from_attributes = True
8585

8686
# These 'synthetic' fields are generated from other model properties. Transform data from other properties as needed, setting
87-
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created.
87+
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created. Only perform these
88+
# transformations if the relevant attributes are present on the input data (i.e., when creating from an ORM object).
8889
@model_validator(mode="before")
8990
def generate_contribution_role_user_relationships(cls, data: Any):
90-
try:
91-
user_associations = transform_contribution_role_associations_to_roles(data.user_associations)
92-
for k, v in user_associations.items():
93-
data.__setattr__(k, v)
94-
95-
except AttributeError as exc:
96-
raise ValidationError(
97-
f"Unable to create {cls.__name__} without attribute: {exc}." # type: ignore
98-
)
91+
if hasattr(data, "user_associations"):
92+
try:
93+
user_associations = transform_contribution_role_associations_to_roles(data.user_associations)
94+
for k, v in user_associations.items():
95+
data.__setattr__(k, v)
96+
97+
except (AttributeError, KeyError) as exc:
98+
raise ValidationError(f"Unable to coerce user associations for {cls.__name__}: {exc}.")
9999
return data
100100

101101
@model_validator(mode="before")
102102
def generate_score_set_urn_list(cls, data: Any):
103-
if not hasattr(data, "score_set_urns"):
103+
if hasattr(data, "score_sets"):
104104
try:
105105
data.__setattr__("score_set_urns", transform_score_set_list_to_urn_list(data.score_sets))
106-
except AttributeError as exc:
107-
raise ValidationError(f"Unable to create {cls.__name__} without attribute: {exc}.") # type: ignore
106+
except (AttributeError, KeyError) as exc:
107+
raise ValidationError(f"Unable to coerce score set urns for {cls.__name__}: {exc}.")
108108
return data
109109

110110
@model_validator(mode="before")
111111
def generate_experiment_urn_list(cls, data: Any):
112-
if not hasattr(data, "experiment_urns"):
112+
if hasattr(data, "experiments"):
113113
try:
114114
data.__setattr__("experiment_urns", transform_experiment_list_to_urn_list(data.experiments))
115-
except AttributeError as exc:
116-
raise ValidationError(f"Unable to create {cls.__name__} without attribute: {exc}.") # type: ignore
115+
except (AttributeError, KeyError) as exc:
116+
raise ValidationError(f"Unable to coerce experiment urns for {cls.__name__}: {exc}.")
117117
return data
118118

119119

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from mavedb.models.enums.contribution_role import ContributionRole
5+
from mavedb.view_models.collection import Collection, SavedCollection
6+
from tests.helpers.constants import TEST_COLLECTION_RESPONSE
7+
from tests.helpers.util.common import dummy_attributed_object_from_dict
8+
9+
10+
@pytest.mark.parametrize(
11+
"exclude,expected_missing_fields",
12+
[
13+
("user_associations", ["admins", "editors", "viewers"]),
14+
("score_sets", ["scoreSetUrns"]),
15+
("experiments", ["experimentUrns"]),
16+
],
17+
)
18+
def test_cannot_create_saved_experiment_without_all_attributed_properties(exclude, expected_missing_fields):
19+
collection = TEST_COLLECTION_RESPONSE.copy()
20+
collection["urn"] = "urn:mavedb:collection-xxx"
21+
22+
# Remove pre-existing synthetic properties
23+
collection.pop("experimentUrns", None)
24+
collection.pop("scoreSetUrns", None)
25+
collection.pop("admins", None)
26+
collection.pop("editors", None)
27+
collection.pop("viewers", None)
28+
29+
# Set synthetic properties with dummy attributed objects to mock SQLAlchemy model objects.
30+
collection["experiments"] = [dummy_attributed_object_from_dict({"urn": "urn:mavedb:experiment-xxx"})]
31+
collection["score_sets"] = [
32+
dummy_attributed_object_from_dict({"urn": "urn:mavedb:score_set-xxx", "superseding_score_set": None})
33+
]
34+
collection["user_associations"] = [
35+
dummy_attributed_object_from_dict(
36+
{
37+
"contribution_role": ContributionRole.admin,
38+
"user": {"id": 1, "username": "test_user", "email": "[email protected]"},
39+
}
40+
),
41+
dummy_attributed_object_from_dict(
42+
{
43+
"contribution_role": ContributionRole.editor,
44+
"user": {"id": 1, "username": "test_user", "email": "[email protected]"},
45+
}
46+
),
47+
dummy_attributed_object_from_dict(
48+
{
49+
"contribution_role": ContributionRole.viewer,
50+
"user": {"id": 1, "username": "test_user", "email": "[email protected]"},
51+
}
52+
),
53+
]
54+
55+
collection.pop(exclude)
56+
collection_attributed_object = dummy_attributed_object_from_dict(collection)
57+
with pytest.raises(ValidationError) as exc_info:
58+
SavedCollection.model_validate(collection_attributed_object)
59+
60+
# Should fail with missing fields coerced from missing attributed properties
61+
msg = str(exc_info.value)
62+
assert "Field required" in msg
63+
for field in expected_missing_fields:
64+
assert field in msg
65+
66+
67+
def test_saved_collection_can_be_created_with_all_attributed_properties():
68+
collection = TEST_COLLECTION_RESPONSE.copy()
69+
urn = "urn:mavedb:collection-xxx"
70+
collection["urn"] = urn
71+
72+
# Remove pre-existing synthetic properties
73+
collection.pop("experimentUrns", None)
74+
collection.pop("scoreSetUrns", None)
75+
collection.pop("admins", None)
76+
collection.pop("editors", None)
77+
collection.pop("viewers", None)
78+
79+
# Set synthetic properties with dummy attributed objects to mock SQLAlchemy model objects.
80+
collection["experiments"] = [dummy_attributed_object_from_dict({"urn": "urn:mavedb:experiment-xxx"})]
81+
collection["score_sets"] = [
82+
dummy_attributed_object_from_dict({"urn": "urn:mavedb:score_set-xxx", "superseding_score_set": None})
83+
]
84+
collection["user_associations"] = [
85+
dummy_attributed_object_from_dict(
86+
{
87+
"contribution_role": ContributionRole.admin,
88+
"user": {"id": 1, "username": "test_user", "email": "[email protected]"},
89+
}
90+
),
91+
dummy_attributed_object_from_dict(
92+
{
93+
"contribution_role": ContributionRole.editor,
94+
"user": {"id": 1, "username": "test_user", "email": "[email protected]"},
95+
}
96+
),
97+
dummy_attributed_object_from_dict(
98+
{
99+
"contribution_role": ContributionRole.viewer,
100+
"user": {"id": 1, "username": "test_user", "email": "[email protected]"},
101+
}
102+
),
103+
]
104+
105+
collection_attributed_object = dummy_attributed_object_from_dict(collection)
106+
model = SavedCollection.model_validate(collection_attributed_object)
107+
assert model.name == TEST_COLLECTION_RESPONSE["name"]
108+
assert model.urn == urn
109+
assert len(model.admins) == 1
110+
assert len(model.editors) == 1
111+
assert len(model.viewers) == 1
112+
assert len(model.experiment_urns) == 1
113+
assert len(model.score_set_urns) == 1
114+
115+
116+
def test_collection_can_be_created_from_non_orm_context():
117+
data = dict(TEST_COLLECTION_RESPONSE)
118+
data["urn"] = "urn:mavedb:collection-xxx"
119+
model = Collection.model_validate(data)
120+
assert model.urn == data["urn"]

0 commit comments

Comments
 (0)