Skip to content

Commit 699a8d5

Browse files
committed
Releases pydantic pin, upgrades to pydantic v2 Refs #54
1 parent 4424b63 commit 699a8d5

File tree

6 files changed

+46
-44
lines changed

6 files changed

+46
-44
lines changed

dicomtrolley/core.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
TypeVar,
1414
)
1515

16-
from pydantic import Field, ValidationError
17-
from pydantic.class_validators import validator
16+
from pydantic import Field, ValidationError, field_validator
1817
from pydantic.main import BaseModel
1918
from pydicom.datadict import tag_for_keyword
2019
from pydicom.dataset import Dataset
@@ -608,8 +607,8 @@ class Query(BaseModel):
608607
query_level: QueryLevels = (
609608
QueryLevels.STUDY
610609
) # to which depth to return results
611-
max_study_date: Optional[datetime]
612-
min_study_date: Optional[datetime]
610+
max_study_date: Optional[datetime] = None
611+
min_study_date: Optional[datetime] = None
613612
include_fields: List[str] = Field([]) #
614613

615614
class Config:
@@ -657,7 +656,8 @@ def validate_keyword(keyword):
657656
if not tag_for_keyword(keyword):
658657
raise ValueError(f"{keyword} is not a valid DICOM keyword")
659658

660-
@validator("include_fields")
659+
@field_validator("include_fields")
660+
@classmethod
661661
def include_fields_check(cls, include_fields, values): # noqa: B902, N805
662662
"""Include fields should be valid dicom tag names"""
663663
for field in include_fields:
@@ -684,7 +684,7 @@ class ExtendedQuery(Query):
684684
StudyDescription: str = ""
685685
SeriesDescription: str = ""
686686
InstitutionalDepartmentName: str = ""
687-
PatientBirthDate: Optional[date]
687+
PatientBirthDate: Optional[date] = None
688688

689689

690690
class Searcher:

dicomtrolley/mint.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from xml.etree import ElementTree
77
from xml.etree.ElementTree import ParseError
88

9-
from pydantic.class_validators import root_validator
9+
from pydantic import model_validator
1010
from pydicom.dataelem import DataElement
1111
from pydicom.dataset import Dataset
1212

@@ -159,11 +159,11 @@ class MintQuery(ExtendedQuery):
159159

160160
limit: int = 0 # how many results to return. 0 = all
161161

162-
@root_validator()
162+
@model_validator(mode="after")
163163
def min_max_study_date_xor(cls, values): # noqa: B902, N805
164164
"""Min and max should both be given or both be empty"""
165-
min_date = values.get("min_study_date")
166-
max_date = values.get("max_study_date")
165+
min_date = values.min_study_date
166+
max_date = values.max_study_date
167167
if min_date and not max_date:
168168
raise ValueError(
169169
f"min_study_date parameter was passed"
@@ -177,14 +177,18 @@ def min_max_study_date_xor(cls, values): # noqa: B902, N805
177177
)
178178
return values
179179

180-
@root_validator()
181-
def include_fields_check(cls, values): # noqa: B902, N805
180+
@model_validator(mode="after")
181+
def include_fields_check(self):
182182
"""Include fields should match query level"""
183-
include_fields = values.get("include_fields")
183+
if isinstance(self, list):
184+
# Interplay with base Query field_validator for include fields
185+
return self # don't check
186+
else:
187+
include_fields = self.include_fields
184188
if not include_fields:
185-
return values # May not exist if include_fields is invalid type
189+
return self # May not exist if include_fields is invalid type
186190

187-
query_level = values.get("query_level")
191+
query_level = self.query_level
188192
if query_level: # May be None for child classes
189193
valid_fields = get_valid_fields(query_level=query_level)
190194
for field in include_fields:
@@ -193,7 +197,7 @@ def include_fields_check(cls, values): # noqa: B902, N805
193197
f'"{field}" is not a valid include field for query '
194198
f"level {query_level}. Valid fields: {valid_fields}"
195199
)
196-
return values
200+
return self
197201

198202
def __str__(self):
199203
return str(self.as_parameters())

dicomtrolley/qido_rs.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from datetime import datetime
1212
from typing import Dict, List, Optional, Sequence, Union
1313

14-
from pydantic import root_validator
14+
from pydantic import model_validator
1515
from pydicom import Dataset
1616
from requests import Response
1717

@@ -39,11 +39,11 @@ class QidoRSQueryBase(Query):
3939
limit: int = 0 # How many results to return. 0 = all
4040
offset: int = 0 # Number of skipped results
4141

42-
@root_validator() # type: ignore
43-
def min_max_study_date_xor(cls, values): # noqa: B902, N805
42+
@model_validator(mode="after")
43+
def min_max_study_date_xor(self): # noqa: B902, N805
4444
"""Min and max should both be given or both be empty"""
45-
min_date = values.get("min_study_date")
46-
max_date = values.get("max_study_date")
45+
min_date = self.min_study_date
46+
max_date = self.max_study_date
4747
if min_date and not max_date:
4848
raise ValueError(
4949
f"min_study_date parameter was passed"
@@ -55,7 +55,7 @@ def min_max_study_date_xor(cls, values): # noqa: B902, N805
5555
f"max_study_date parameter was passed ({max_date}), "
5656
f"but min_study_date was not. Both need to be given"
5757
)
58-
return values
58+
return self
5959

6060
@staticmethod
6161
def date_to_str(date_in: Optional[datetime]) -> str:
@@ -158,8 +158,8 @@ class HierarchicalQuery(QidoRSQueryBase):
158158
Faster than relationalQuery, but requires more information
159159
"""
160160

161-
@root_validator() # type: ignore
162-
def uids_should_be_hierarchical(cls, values): # noqa: B902, N805
161+
@model_validator(mode="after")
162+
def uids_should_be_hierarchical(self):
163163
"""Any object uids passed should conform to study->series->instance"""
164164
order = ["StudyInstanceUID", "SeriesInstanceUID", "SOPInstanceUID"]
165165

@@ -182,14 +182,13 @@ def assert_parents_filled(a_hierarchy, value_dict):
182182
else:
183183
return assert_parents_filled(a_hierarchy, value_dict)
184184

185-
assert_parents_filled(order, values)
185+
assert_parents_filled(order, self.dict())
186+
return self
186187

187-
return values
188-
189-
@root_validator() # type: ignore
190-
def uids_should_match_query_level(cls, values): # noqa: B902, N805
188+
@model_validator(mode="after")
189+
def uids_should_match_query_level(self):
191190
"""If a query is for instance level, there should be study and series UIDs"""
192-
query_level = values["query_level"]
191+
query_level = self.query_level
193192

194193
def assert_key_exists(values_in, query_level_in, missing_key_in):
195194
if not values_in.get(missing_key_in):
@@ -199,6 +198,7 @@ def assert_key_exists(values_in, query_level_in, missing_key_in):
199198
f"a QIDO-RS relational query"
200199
)
201200

201+
values = self.dict()
202202
if query_level == QueryLevels.STUDY:
203203
pass # Fine. you can always look for some studies
204204
elif query_level == QueryLevels.SERIES:
@@ -207,7 +207,7 @@ def assert_key_exists(values_in, query_level_in, missing_key_in):
207207
assert_key_exists(values, query_level, "SeriesInstanceUID")
208208
assert_key_exists(values, query_level, "StudyInstanceUID")
209209

210-
return values
210+
return self
211211

212212
def uri_base(self) -> str:
213213
"""WADO-RS url to call when performing this query. Full URI also needs
@@ -294,17 +294,15 @@ class RelationalQuery(QidoRSQueryBase):
294294
Allows broader searches than HierarchicalQuery, but can be slower
295295
"""
296296

297-
@root_validator() # type: ignore
298-
def query_level_should_be_series_or_instance(
299-
cls, values # noqa: B902, N805
300-
):
297+
@model_validator(mode="after")
298+
def query_level_should_be_series_or_instance(self):
301299
"""A relational query only makes sense for the instance and series levels.
302300
If you want to look for studies, us a hierarchical query
303301
"""
304-
if values.get("query_level") == QueryLevels.STUDY:
302+
if self.query_level == QueryLevels.STUDY:
305303
raise ValueError(STUDY_VALUE_ERROR_TEXT)
306304

307-
return values
305+
return self
308306

309307
def uri_base(self) -> str:
310308
"""WADO-RS url to call when performing this query. Full URI also needs

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ requests-futures = "^1.0.0"
1919
pynetdicom = "^1.5.6"
2020
Jinja2 = "^3.0.3"
2121
requests-toolbelt = "^1.0.0"
22-
pydantic = "1.8.2"
22+
pydantic = "^2.9.1"
2323

2424
[tool.poetry.dev-dependencies]
2525
pytest = "^7.4.0"

tests/test_integration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def test_dicom_query_mint_cast(requests_mock, a_mint):
3939
set_mock_response(requests_mock, MINT_SEARCH_INSTANCE_LEVEL_ANY)
4040
with pytest.raises(DICOMTrolleyError):
4141
# should fail, casting to mint would lose unsupported StudyID parameter
42-
a_mint.find_studies(DICOMQuery(StudyID=123))
42+
a_mint.find_studies(DICOMQuery(StudyID="123"))
4343

4444

4545
def test_from_query():

tests/test_rad69.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ def test_rad69_error_from_server(
8585
with pytest.raises(DICOMTrolleyError) as e:
8686
a_rad69.get_dataset(
8787
InstanceReference(
88-
study_uid=1,
89-
series_uid=2,
90-
instance_uid=3,
88+
study_uid="1",
89+
series_uid="2",
90+
instance_uid="3",
9191
)
9292
)
9393
assert re.match(error_contains, str(e))
@@ -244,8 +244,8 @@ def test_wado_datasets_async(a_rad69, requests_mock):
244244
)
245245

246246
instances = [
247-
InstanceReference(study_uid=1, series_uid=2, instance_uid=3),
248-
InstanceReference(study_uid=4, series_uid=5, instance_uid=6),
247+
InstanceReference(study_uid="1", series_uid="2", instance_uid="3"),
248+
InstanceReference(study_uid="4", series_uid="5", instance_uid="6"),
249249
]
250250
a_rad69.use_async = True
251251
datasets = [x for x in a_rad69.datasets(instances)]

0 commit comments

Comments
 (0)