Skip to content

Commit 656ad80

Browse files
committed
fix: robust synthetic property handling for target genes
- Refactored SavedTargetGene and TargetGeneWithScoreSetUrn to synthesize synthetic fields (e.g., external_identifiers, score_set_urn) only from ORM objects, not dicts. - Updated model validators to require either target_sequence or target_accession for all construction contexts. - Added tests to ensure SavedTargetGene and TargetGeneWithScoreSetUrn can be created from both ORM (attributed object) and non-ORM (dict) contexts.
1 parent 09d1562 commit 656ad80

File tree

2 files changed

+117
-11
lines changed

2 files changed

+117
-11
lines changed

src/mavedb/view_models/target_gene.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,16 @@ class Config:
6969
arbitrary_types_allowed = True
7070

7171
# These 'synthetic' fields are generated from other model properties. Transform data from other properties as needed, setting
72-
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created.
72+
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created. Only perform these
73+
# transformations if the relevant attributes are present on the input data (i.e., when creating from an ORM object).
7374
@model_validator(mode="before")
7475
def generate_external_identifiers_list(cls, data: Any):
75-
if not hasattr(data, "external_identifiers"):
76+
if hasattr(data, "ensembl_offset") or hasattr(data, "refseq_offset") or hasattr(data, "uniprot_offset"):
7677
try:
7778
data.__setattr__("external_identifiers", transform_external_identifier_offsets_to_list(data))
78-
except AttributeError as exc:
79+
except (AttributeError, KeyError) as exc:
7980
raise ValidationError(
80-
f"Unable to create {cls.__name__} without attribute: {exc}." # type: ignore
81+
f"Unable to coerce external identifiers for {cls.__name__}: {exc}." # type: ignore
8182
)
8283
return data
8384

@@ -108,15 +109,16 @@ class TargetGeneWithScoreSetUrn(TargetGene):
108109
score_set_urn: str
109110

110111
# These 'synthetic' fields are generated from other model properties. Transform data from other properties as needed, setting
111-
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created.
112+
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created. Only perform these
113+
# transformations if the relevant attributes are present on the input data (i.e., when creating from an ORM object).
112114
@model_validator(mode="before")
113115
def generate_score_set_urn(cls, data: Any):
114-
if not hasattr(data, "score_set_urn"):
116+
if hasattr(data, "score_set"):
115117
try:
116118
data.__setattr__("score_set_urn", transform_score_set_to_urn(data.score_set))
117-
except AttributeError as exc:
119+
except (AttributeError, KeyError) as exc:
118120
raise ValidationError(
119-
f"Unable to create {cls.__name__} without attribute: {exc}." # type: ignore
121+
f"Unable to coerce score set urn for {cls.__name__}: {exc}." # type: ignore
120122
)
121123
return data
122124

tests/view_models/test_target_gene.py

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import pytest
22

3-
from mavedb.view_models.target_gene import TargetGeneCreate, SavedTargetGene
3+
from mavedb.view_models.target_gene import SavedTargetGene, TargetGene, TargetGeneCreate, TargetGeneWithScoreSetUrn
44
from tests.helpers.constants import (
55
SEQUENCE,
6+
TEST_ENSEMBLE_EXTERNAL_IDENTIFIER,
67
TEST_POPULATED_TAXONOMY,
7-
TEST_SAVED_TAXONOMY,
88
TEST_REFSEQ_EXTERNAL_IDENTIFIER,
9-
TEST_ENSEMBLE_EXTERNAL_IDENTIFIER,
9+
TEST_SAVED_TAXONOMY,
1010
TEST_UNIPROT_EXTERNAL_IDENTIFIER,
1111
)
1212
from tests.helpers.util.common import dummy_attributed_object_from_dict
@@ -200,3 +200,107 @@ def test_cannot_create_saved_target_without_seq_or_acc():
200200
SavedTargetGene.model_validate(target_gene)
201201

202202
assert "Either a `target_sequence` or `target_accession` is required" in str(exc_info.value)
203+
204+
205+
def test_saved_target_gene_can_be_created_from_orm():
206+
orm_obj = dummy_attributed_object_from_dict(
207+
{
208+
"id": 1,
209+
"name": "UBE2I",
210+
"category": "regulatory",
211+
"ensembl_offset": dummy_attributed_object_from_dict(
212+
{"offset": 1, "identifier": dummy_attributed_object_from_dict(TEST_ENSEMBLE_EXTERNAL_IDENTIFIER)}
213+
),
214+
"refseq_offset": None,
215+
"uniprot_offset": None,
216+
"target_sequence": dummy_attributed_object_from_dict(
217+
{
218+
"sequenceType": "dna",
219+
"sequence": SEQUENCE,
220+
"taxonomy": TEST_SAVED_TAXONOMY,
221+
}
222+
),
223+
"target_accession": None,
224+
"record_type": "target_gene",
225+
"uniprot_id_from_mapped_metadata": None,
226+
}
227+
)
228+
model = SavedTargetGene.model_validate(orm_obj)
229+
assert model.name == "UBE2I"
230+
assert model.external_identifiers[0].identifier.identifier == "ENSG00000103275"
231+
232+
233+
def test_target_gene_with_score_set_urn_can_be_created_from_orm():
234+
orm_obj = dummy_attributed_object_from_dict(
235+
{
236+
"id": 1,
237+
"name": "UBE2I",
238+
"category": "regulatory",
239+
"ensembl_offset": dummy_attributed_object_from_dict(
240+
{
241+
"offset": 1,
242+
"identifier": dummy_attributed_object_from_dict(TEST_ENSEMBLE_EXTERNAL_IDENTIFIER),
243+
}
244+
),
245+
"refseq_offset": None,
246+
"uniprot_offset": None,
247+
"target_sequence": dummy_attributed_object_from_dict(
248+
{
249+
"sequenceType": "dna",
250+
"sequence": SEQUENCE,
251+
"taxonomy": TEST_SAVED_TAXONOMY,
252+
}
253+
),
254+
"target_accession": None,
255+
"record_type": "target_gene",
256+
"uniprot_id_from_mapped_metadata": None,
257+
"score_set": dummy_attributed_object_from_dict({"urn": "urn:mavedb:01234567-a-1"}),
258+
}
259+
)
260+
model = TargetGeneWithScoreSetUrn.model_validate(orm_obj)
261+
assert model.name == "UBE2I"
262+
assert model.score_set_urn == "urn:mavedb:01234567-a-1"
263+
264+
265+
def test_target_gene_can_be_created_from_non_orm_context():
266+
# Minimal valid dict for TargetGene (must have target_sequence or target_accession)
267+
data = {
268+
"id": 1,
269+
"name": "UBE2I",
270+
"category": "regulatory",
271+
"external_identifiers": [{"identifier": TEST_ENSEMBLE_EXTERNAL_IDENTIFIER, "offset": 1}],
272+
"target_sequence": {
273+
"sequenceType": "dna",
274+
"sequence": SEQUENCE,
275+
"taxonomy": TEST_SAVED_TAXONOMY,
276+
},
277+
"target_accession": None,
278+
"record_type": "target_gene",
279+
"uniprot_id_from_mapped_metadata": None,
280+
}
281+
model = TargetGene.model_validate(data)
282+
assert model.name == data["name"]
283+
assert model.category == data["category"]
284+
assert model.external_identifiers[0].identifier.identifier == "ENSG00000103275"
285+
286+
287+
def test_target_gene_with_score_set_urn_can_be_created_from_dict():
288+
# Minimal valid dict for TargetGeneWithScoreSetUrn (must have target_sequence or target_accession)
289+
data = {
290+
"id": 1,
291+
"name": "UBE2I",
292+
"category": "regulatory",
293+
"external_identifiers": [{"identifier": TEST_ENSEMBLE_EXTERNAL_IDENTIFIER, "offset": 1}],
294+
"target_sequence": {
295+
"sequenceType": "dna",
296+
"sequence": SEQUENCE,
297+
"taxonomy": TEST_SAVED_TAXONOMY,
298+
},
299+
"target_accession": None,
300+
"record_type": "target_gene",
301+
"uniprot_id_from_mapped_metadata": None,
302+
"score_set_urn": "urn:mavedb:01234567-a-1",
303+
}
304+
model = TargetGeneWithScoreSetUrn.model_validate(data)
305+
assert model.name == data["name"]
306+
assert model.score_set_urn == data["score_set_urn"]

0 commit comments

Comments
 (0)