nci-cidc-api-modules 1.2.51__py3-none-any.whl → 1.2.52__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cidc_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.51"
1
+ __version__ = "1.2.52"
@@ -1,7 +1,11 @@
1
- from pydantic import BaseModel, ConfigDict
1
+ import copy
2
2
  from contextlib import contextmanager
3
+ from typing import Self, ClassVar
4
+ from pydantic import BaseModel, ConfigDict, model_validator, ValidationError
5
+ from pydantic_core import InitErrorDetails
3
6
 
4
- import copy
7
+ from cidc_api.models.errors import ValueLocError
8
+ from functools import wraps
5
9
 
6
10
 
7
11
  class Base(BaseModel):
@@ -11,6 +15,7 @@ class Base(BaseModel):
11
15
  from_attributes=True,
12
16
  extra="allow",
13
17
  )
18
+ forced_validators: ClassVar = []
14
19
 
15
20
  # Validates the new state and updates the object if valid
16
21
  def update(self, **kwargs):
@@ -33,6 +38,8 @@ class Base(BaseModel):
33
38
  self.__dict__.update(original_dict)
34
39
  raise
35
40
 
41
+ # CM that delays validation until all fields are applied.
42
+
36
43
  @classmethod
37
44
  def split_list(cls, val):
38
45
  """Listify fields that are multi-valued in input data, e.g. 'lung|kidney'"""
@@ -46,3 +53,57 @@ class Base(BaseModel):
46
53
  return []
47
54
  else:
48
55
  raise ValueError("Field value must be string or list")
56
+
57
+ @model_validator(mode="wrap")
58
+ @classmethod
59
+ def check_all_forced_validators(cls, data, handler, info) -> Self:
60
+ """This base validator ensures all registered forced_validator(s) get called along with
61
+ normal model field validators no matter what the outcome of either is. Normal field_validator
62
+ and model_validator decorators don't guarantee this. We want to collect as many errors as we can.
63
+
64
+ Collects errors from both attempts and raises them in a combined ValidationError."""
65
+
66
+ validation_errors = []
67
+ # When assigning attributes to an already-hydrated model, data is an instance of the model
68
+ # instead of a dict and handler is an AssignmentValidatorCallable instead of a ValidatorCallable(!!)
69
+ extracted_data = data.model_dump() if not isinstance(data, dict) else data
70
+
71
+ for validator in cls.forced_validators:
72
+ try:
73
+ func_name = validator.__name__
74
+ func = getattr(cls, func_name)
75
+ func(extracted_data, info)
76
+ except (ValueError, ValueLocError) as e:
77
+ validation_errors.append(
78
+ InitErrorDetails(
79
+ type="value_error",
80
+ loc=e.loc,
81
+ input=extracted_data,
82
+ ctx={"error": e},
83
+ )
84
+ )
85
+ try:
86
+ # Instantiate the model to ensure all other normal field validations are called
87
+ retval = handler(data)
88
+ except ValidationError as e:
89
+ validation_errors.extend(e.errors())
90
+ if validation_errors:
91
+ raise ValidationError.from_exception_data(title=cls.__name__, line_errors=validation_errors)
92
+ return retval
93
+
94
+
95
+ def forced_validator(func):
96
+ """A method marked with this decorator is added to the class list of forced_validators"""
97
+ func._is_forced_validator = True # Tag the function object with a custom attribute
98
+ return func
99
+
100
+
101
+ def forced_validators(cls):
102
+ """A class marked with this decorator accumulates its methods marked with force_validator into a list
103
+ for later invocation."""
104
+
105
+ cls.forced_validators = []
106
+ for obj in cls.__dict__.values():
107
+ if getattr(obj, "_is_forced_validator", False):
108
+ cls.forced_validators.append(obj)
109
+ return cls
@@ -1,9 +1,8 @@
1
- from typing import Self
2
-
3
- from pydantic import Field, NonNegativeInt, model_validator
1
+ from pydantic import NonNegativeInt
4
2
 
5
3
  from cidc_api.models.pydantic.base import Base
6
4
  from cidc_api.reference.ctcae import is_ctcae_other_term
5
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
7
6
  from cidc_api.models.errors import ValueLocError
8
7
  from cidc_api.models.types import (
9
8
  CTCAEEventTerm,
@@ -18,6 +17,7 @@ from cidc_api.models.types import (
18
17
  )
19
18
 
20
19
 
20
+ @forced_validators
21
21
  class AdverseEvent(Base):
22
22
  __data_category__ = "adverse_event"
23
23
  __cardinality__ = "many"
@@ -76,44 +76,52 @@ class AdverseEvent(Base):
76
76
  # The individual therapy (therapy agent, radiotherapy, surgery, stem cell transplant) in the treatment that is attributed to the adverse event.
77
77
  individual_therapy: str | None = None
78
78
 
79
- @model_validator(mode="after")
80
- def validate_term_and_code_cr(self) -> Self:
81
- if not self.event_term and not self.event_code:
79
+ @forced_validator
80
+ @classmethod
81
+ def validate_term_and_code_cr(cls, data, info) -> None:
82
+ event_code = data.get("event_code", None)
83
+ event_term = data.get("event_term", None)
84
+
85
+ if not event_term and not event_code:
82
86
  raise ValueLocError(
83
87
  "Please provide event_term or event_code or both",
84
- loc="event_term",
88
+ loc="event_term,event_code",
85
89
  )
86
- return self
87
-
88
- @model_validator(mode="after")
89
- def validate_event_other_specify_cr(self) -> Self:
90
- if (
91
- self.severity_grade_system == "CTCAE"
92
- and is_ctcae_other_term(self.event_term)
93
- and not self.event_other_specify
94
- ):
90
+
91
+ @forced_validator
92
+ @classmethod
93
+ def validate_event_other_specify_cr(cls, data, info) -> None:
94
+ event_other_specify = data.get("event_other_specify", None)
95
+ severity_grade_system = data.get("severity_grade_system", None)
96
+ event_term = data.get("event_term", None)
97
+
98
+ if severity_grade_system == "CTCAE" and is_ctcae_other_term(event_term) and not event_other_specify:
95
99
  raise ValueLocError(
96
100
  'If severity_grade_system is "CTCAE" and the event_code or event_term are of type '
97
101
  '"Other, specify", please provide event_other_specify',
98
102
  loc="event_other_specify",
99
103
  )
100
- return self
101
104
 
102
- @model_validator(mode="after")
103
- def validate_system_organ_class_cr(self) -> Self:
104
- if self.event_other_specify and not self.system_organ_class:
105
+ @forced_validator
106
+ @classmethod
107
+ def validate_system_organ_class_cr(cls, data, info) -> None:
108
+ event_other_specify = data.get("event_other_specify", None)
109
+ system_organ_class = data.get("system_organ_class", None)
110
+
111
+ if event_other_specify and not system_organ_class:
105
112
  raise ValueLocError(
106
- "If event_other_specify is provided, please provide system_organ_class.",
107
- loc="system_organ_class",
113
+ "If event_other_specify is provided, please provide system_organ_class.", loc="system_organ_class"
108
114
  )
109
- return self
110
115
 
111
- @model_validator(mode="after")
112
- def validate_days_to_resolution_of_event_chronology(self) -> Self:
113
- if self.days_to_resolution_of_event is not None and self.days_to_onset_of_event is not None:
114
- if self.days_to_resolution_of_event < self.days_to_onset_of_event:
116
+ @forced_validator
117
+ @classmethod
118
+ def validate_days_to_resolution_of_event_chronology(cls, data, info) -> None:
119
+ days_to_onset_of_event = data.get("days_to_onset_of_event", None)
120
+ days_to_resolution_of_event = data.get("days_to_resolution_of_event", None)
121
+
122
+ if days_to_resolution_of_event is not None and days_to_onset_of_event is not None:
123
+ if int(days_to_resolution_of_event) < int(days_to_onset_of_event):
115
124
  raise ValueLocError(
116
125
  'Violate "days_to_onset_of_event" <= "days_to_resolution_of_event"',
117
126
  loc="days_to_resolution_of_event",
118
127
  )
119
- return self
@@ -1,12 +1,13 @@
1
- from typing import Self, Any
1
+ from typing import Any
2
2
 
3
- from pydantic import model_validator, field_validator
3
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
4
 
5
5
  from cidc_api.models.errors import ValueLocError
6
6
  from cidc_api.models.pydantic.base import Base
7
7
  from cidc_api.models.types import ICD10CMCode, ICD10CMTerm
8
8
 
9
9
 
10
+ @forced_validators
10
11
  class Comorbidity(Base):
11
12
  __data_category__ = "comorbidity"
12
13
  __cardinality__ = "many"
@@ -28,11 +29,15 @@ class Comorbidity(Base):
28
29
  # A descriptive string that names or briefly describes the comorbidity.
29
30
  comorbidity_other: str | None = None
30
31
 
31
- @model_validator(mode="after")
32
- def validate_code_or_term_or_other_cr(self) -> Self:
33
- if not self.comorbidity_code and not self.comorbidity_term and not self.comorbidity_other:
32
+ @forced_validator
33
+ @classmethod
34
+ def validate_code_or_term_or_other_cr(cls, data, info) -> None:
35
+ comorbidity_term = data.get("comorbidity_term", None)
36
+ comorbidity_other = data.get("comorbidity_other", None)
37
+ comorbidity_code = data.get("comorbidity_code", None)
38
+
39
+ if not comorbidity_code and not comorbidity_term and not comorbidity_other:
34
40
  raise ValueLocError(
35
41
  'Please provide at least one of "comorbidity_code", "comorbidity_term" or "comorbidity_other".',
36
- loc="comorbidity_code",
42
+ loc="comorbidity_code,comorbidity_term,comorbidity_other",
37
43
  )
38
- return self
@@ -1,6 +1,7 @@
1
- from typing import Self, Annotated, List
1
+ from typing import Annotated, List
2
2
 
3
- from pydantic import PositiveInt, NonNegativeFloat, PositiveFloat, model_validator, field_validator, BeforeValidator
3
+ from pydantic import PositiveInt, NonNegativeFloat, PositiveFloat, BeforeValidator
4
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
5
 
5
6
  from cidc_api.models.errors import ValueLocError
6
7
  from cidc_api.models.pydantic.base import Base
@@ -17,6 +18,7 @@ from cidc_api.models.types import (
17
18
  )
18
19
 
19
20
 
21
+ @forced_validators
20
22
  class Demographic(Base):
21
23
  __data_category__ = "demographic"
22
24
  __cardinality__ = "one"
@@ -91,40 +93,48 @@ class Demographic(Base):
91
93
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2681552%20and%20ver_nr=1
92
94
  highest_level_of_education: Education | None = None
93
95
 
94
- @model_validator(mode="after")
95
- def validate_age_at_enrollment_cr(self) -> Self:
96
- if self.age_90_or_over:
97
- if self.age_at_enrollment or self.age_at_enrollment_units:
96
+ @forced_validator
97
+ @classmethod
98
+ def validate_age_at_enrollment_cr(cls, data, info) -> None:
99
+ age_at_enrollment_units = data.get("age_at_enrollment_units", None)
100
+ age_90_or_over = data.get("age_90_or_over", None)
101
+ age_at_enrollment = int(data.get("age_at_enrollment", None) or 0)
102
+
103
+ if age_90_or_over:
104
+ if age_at_enrollment or age_at_enrollment_units:
98
105
  raise ValueLocError(
99
106
  'If "age_90_or_over" is "Yes" then "age_at_enrollment" and "age_at_enrollment_units" must be blank.',
100
107
  loc="age_at_enrollment",
101
108
  )
102
109
  else:
103
- if not self.age_at_enrollment or not self.age_at_enrollment_units:
110
+ if not age_at_enrollment or not age_at_enrollment_units:
104
111
  raise ValueLocError(
105
112
  'If "age_90_or_over" is "No" then "age_at_enrollment" and "age_at_enrollment_units" are required.',
106
- loc="age_at_enrollment_units",
113
+ loc="age_at_enrollment",
107
114
  )
108
- return self
109
115
 
110
- @model_validator(mode="after")
111
- def validate_age_at_enrollment_value(self) -> Self:
112
- if not self.age_90_or_over:
113
- age_in_years = (
114
- self.age_at_enrollment if self.age_at_enrollment_units == "Years" else self.age_at_enrollment / 365.25
115
- )
116
+ @forced_validator
117
+ @classmethod
118
+ def validate_age_at_enrollment_value(cls, data, info) -> None:
119
+ age_at_enrollment_units = data.get("age_at_enrollment_units", None)
120
+ age_90_or_over = data.get("age_90_or_over", None)
121
+ age_at_enrollment = int(data.get("age_at_enrollment", None) or 0)
122
+
123
+ if not age_90_or_over:
124
+ age_in_years = age_at_enrollment if age_at_enrollment_units == "Years" else age_at_enrollment / 365.25
116
125
  if age_in_years >= 90:
117
126
  raise ValueLocError(
118
127
  '"age_at_enrollment" cannot represent a value greater than 90 years of age.',
119
128
  loc="age_at_enrollment",
120
129
  )
121
- return self
122
130
 
123
- @model_validator(mode="after")
124
- def validate_body_surface_area_units_cr(self) -> Self:
125
- if self.body_surface_area and not self.body_surface_area_units:
131
+ @forced_validator
132
+ @classmethod
133
+ def validate_body_surface_area_units_cr(cls, data, info) -> None:
134
+ body_surface_area = data.get("body_surface_area", None)
135
+ body_surface_area_units = data.get("body_surface_area_units", None)
136
+
137
+ if body_surface_area and not body_surface_area_units:
126
138
  raise ValueLocError(
127
- 'If "body_surface_area" is provided then "body_surface_area_units_other" must also be provided.',
128
- loc="body_surface_area_units_other",
139
+ 'If "body_surface_area" is provided then "body_surface_area_units" must also be provided.'
129
140
  )
130
- return self
@@ -1,5 +1,7 @@
1
- from pydantic import NonPositiveInt, model_validator, BeforeValidator
2
- from typing import List, Self, Annotated, get_args
1
+ from pydantic import NonPositiveInt, BeforeValidator
2
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
3
+
4
+ from typing import List, Self, Annotated, get_args, Any
3
5
 
4
6
  from cidc_api.models.errors import ValueLocError
5
7
  from cidc_api.models.pydantic.base import Base
@@ -21,6 +23,7 @@ from cidc_api.models.types import (
21
23
  )
22
24
 
23
25
 
26
+ @forced_validators
24
27
  class Disease(Base):
25
28
  __data_category__ = "disease"
26
29
  __cardinality__ = "many"
@@ -93,82 +96,105 @@ class Disease(Base):
93
96
 
94
97
  extramedullary_organ: Annotated[List[UberonAnatomicalTerm] | None, BeforeValidator(Base.split_list)] = []
95
98
 
96
- @model_validator(mode="after")
97
- def validate_code_or_term_or_description_cr(self) -> Self:
98
- if not self.morphological_code and not self.morphological_term and not self.cancer_type_description:
99
+ @forced_validator
100
+ @classmethod
101
+ def validate_code_or_term_or_description_cr(cls, data, info) -> None:
102
+ morphological_term = data.get("morphological_term", None)
103
+ cancer_type_description = data.get("cancer_type_description", None)
104
+ morphological_code = data.get("morphological_code", None)
105
+
106
+ if not morphological_code and not morphological_term and not cancer_type_description:
99
107
  raise ValueLocError(
100
108
  'Please provide at least one of "morphological_code", "morphological_term" or "cancer_type_description".',
101
109
  loc="morphological_code",
102
110
  )
103
- return self
104
111
 
105
- @model_validator(mode="after")
106
- def validate_cancer_stage_system_version(self) -> Self:
107
- msg = f"{self.cancer_stage_system_version} is not applicable to {self.cancer_stage_system}"
108
- if self.cancer_stage_system == "AJCC" and self.cancer_stage_system_version not in get_args(
109
- CancerStageSystemVersionAJCC
110
- ):
112
+ @forced_validator
113
+ @classmethod
114
+ def validate_cancer_stage_system_version(cls, data, info) -> None:
115
+ cancer_stage_system_version = data.get("cancer_stage_system_version", None)
116
+ cancer_stage_system = data.get("cancer_stage_system", None)
117
+
118
+ msg = f"{cancer_stage_system_version} is not applicable to {cancer_stage_system}"
119
+ if cancer_stage_system == "AJCC" and cancer_stage_system_version not in get_args(CancerStageSystemVersionAJCC):
111
120
  raise ValueLocError(msg, loc="cancer_stage_system")
112
- elif self.cancer_stage_system == "RISS" and self.cancer_stage_system_version not in get_args(
121
+ elif cancer_stage_system == "RISS" and cancer_stage_system_version not in get_args(
113
122
  CancerStageSystemVersionRISS
114
123
  ):
115
124
  raise ValueLocError(msg, loc="cancer_stage_system")
116
- elif self.cancer_stage_system == "FIGO" and self.cancer_stage_system_version not in get_args(
125
+ elif cancer_stage_system == "FIGO" and cancer_stage_system_version not in get_args(
117
126
  CancerStageSystemVersionFIGO
118
127
  ):
119
128
  raise ValueLocError(msg, loc="cancer_stage_system")
120
- return self
121
129
 
122
- @model_validator(mode="after")
123
- def validate_cancer_stage_system_version_cr(self) -> Self:
124
- if self.cancer_stage_system != "Not Applicable" and not self.cancer_stage_system_version:
130
+ @forced_validator
131
+ @classmethod
132
+ def validate_cancer_stage_system_version_cr(cls, data, info) -> None:
133
+ cancer_stage_system = data.get("cancer_stage_system", None)
134
+ cancer_stage_system_version = data.get("cancer_stage_system_version", None)
135
+
136
+ if cancer_stage_system != "Not Applicable" and not cancer_stage_system_version:
125
137
  raise ValueLocError(
126
- f'Please provide cancer_stage_system_version when cancer_stage_system is "{self.cancer_stage_system}"',
138
+ f'Please provide cancer_stage_system_version when cancer_stage_system is "{cancer_stage_system}"',
127
139
  loc="cancer_stage_system_version",
128
140
  )
129
- return self
130
141
 
131
- @model_validator(mode="after")
132
- def validate_cancer_stage_cr(self) -> Self:
133
- if self.cancer_stage_system != "Not Applicable" and not self.cancer_stage:
142
+ @forced_validator
143
+ @classmethod
144
+ def validate_cancer_stage_cr(cls, data, info) -> None:
145
+ cancer_stage_system = data.get("cancer_stage_system", None)
146
+ cancer_stage = data.get("cancer_stage", None)
147
+
148
+ if cancer_stage_system != "Not Applicable" and not cancer_stage:
134
149
  raise ValueLocError(
135
- f'Please provide cancer_stage when cancer_stage_system is "{self.cancer_stage_system}"',
150
+ f'Please provide cancer_stage when cancer_stage_system is "{cancer_stage_system}"',
136
151
  loc="cancer_stage",
137
152
  )
138
- return self
139
153
 
140
- @model_validator(mode="after")
141
- def validate_t_category_cr(self) -> Self:
142
- if self.cancer_stage_system == "AJCC" and not self.t_category:
154
+ @forced_validator
155
+ @classmethod
156
+ def validate_t_category_cr(cls, data, info) -> None:
157
+ cancer_stage_system = data.get("cancer_stage_system", None)
158
+ t_category = data.get("t_category", None)
159
+
160
+ if cancer_stage_system == "AJCC" and not t_category:
143
161
  raise ValueLocError(
144
- f'Please provide t_category when cancer_stage_system is "{self.cancer_stage_system}"',
162
+ f'Please provide t_category when cancer_stage_system is "{cancer_stage_system}"',
145
163
  loc="t_category",
146
164
  )
147
- return self
148
165
 
149
- @model_validator(mode="after")
150
- def validate_n_category_cr(self) -> Self:
151
- if self.cancer_stage_system == "AJCC" and not self.n_category:
166
+ @forced_validator
167
+ @classmethod
168
+ def validate_n_category_cr(cls, data, info) -> None:
169
+ cancer_stage_system = data.get("cancer_stage_system", None)
170
+ n_category = data.get("n_category", None)
171
+
172
+ if cancer_stage_system == "AJCC" and not n_category:
152
173
  raise ValueLocError(
153
- f'Please provide n_category when cancer_stage_system is "{self.cancer_stage_system}"',
174
+ f'Please provide n_category when cancer_stage_system is "{cancer_stage_system}"',
154
175
  loc="n_category",
155
176
  )
156
- return self
157
177
 
158
- @model_validator(mode="after")
159
- def validate_m_category_cr(self) -> Self:
160
- if self.cancer_stage_system == "AJCC" and not self.m_category:
178
+ @forced_validator
179
+ @classmethod
180
+ def validate_m_category_cr(cls, data, info) -> None:
181
+ cancer_stage_system = data.get("cancer_stage_system", None)
182
+ m_category = data.get("m_category", None)
183
+
184
+ if cancer_stage_system == "AJCC" and not m_category:
161
185
  raise ValueLocError(
162
- f'Please provide m_category when cancer_stage_system is "{self.cancer_stage_system}"',
186
+ f'Please provide m_category when cancer_stage_system is "{cancer_stage_system}"',
163
187
  loc="m_category",
164
188
  )
165
- return self
166
189
 
167
- @model_validator(mode="after")
168
- def validate_extramedullary_organ_cr(self) -> Self:
169
- if self.solely_extramedullary_disease in ["No", "Unknown"] and self.extramedullary_organ:
190
+ @forced_validator
191
+ @classmethod
192
+ def validate_extramedullary_organ_cr(cls, data, info) -> None:
193
+ solely_extramedullary_disease = data.get("solely_extramedullary_disease", None)
194
+ extramedullary_organ = data.get("extramedullary_organ", None)
195
+
196
+ if solely_extramedullary_disease in ["No", "Unknown"] and extramedullary_organ:
170
197
  raise ValueLocError(
171
198
  "If solely_extramedullary_disease indicates no disease, please leave extramedullary_organ blank.",
172
199
  loc="extramedullary_organ",
173
200
  )
174
- return self
@@ -1,12 +1,11 @@
1
- from typing import Self
2
-
3
- from pydantic import model_validator
1
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
2
 
5
3
  from cidc_api.models.errors import ValueLocError
6
4
  from cidc_api.models.pydantic.base import Base
7
5
  from cidc_api.models.types import YNU, ExposureType
8
6
 
9
7
 
8
+ @forced_validators
10
9
  class Exposure(Base):
11
10
  __data_category__ = "exposure"
12
11
  __cardinality__ = "many"
@@ -26,11 +25,14 @@ class Exposure(Base):
26
25
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=15753203%20and%20ver_nr=1
27
26
  exposure_type: ExposureType | None = None
28
27
 
29
- @model_validator(mode="after")
30
- def validate_exposure_type_cr(self) -> Self:
31
- if self.carcinogen_exposure in ["No", "Unknown"] and self.exposure_type:
28
+ @forced_validator
29
+ @classmethod
30
+ def validate_exposure_type_cr(cls, data, info) -> None:
31
+ carcinogen_exposure = data.get("carcinogen_exposure", None)
32
+ exposure_type = data.get("exposure_type", None)
33
+
34
+ if carcinogen_exposure in ["No", "Unknown"] and exposure_type:
32
35
  raise ValueLocError(
33
36
  "If carcinogen_exposure indicates non exposure, please leave exposure_type blank.",
34
37
  loc="exposure_type",
35
38
  )
36
- return self
@@ -1,12 +1,12 @@
1
- from typing import Self
2
-
3
- from pydantic import NonNegativeInt, PositiveFloat, model_validator
1
+ from pydantic import NonNegativeInt, PositiveFloat
2
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
3
 
5
4
  from cidc_api.models.errors import ValueLocError
6
5
  from cidc_api.models.pydantic.base import Base
7
6
  from cidc_api.models.types import TobaccoSmokingStatus
8
7
 
9
8
 
9
+ @forced_validators
10
10
  class MedicalHistory(Base):
11
11
  __data_category__ = "medical_history"
12
12
  __cardinality__ = "one"
@@ -30,11 +30,14 @@ class MedicalHistory(Base):
30
30
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=16089302%20and%20ver_nr=1
31
31
  num_prior_systemic_therapies: NonNegativeInt | None = None
32
32
 
33
- @model_validator(mode="after")
34
- def validate_pack_years_smoked_cr(self) -> Self:
35
- if self.tobacco_smoking_status in ["Never Smoker", "Unknown", "Not reported"] and self.pack_years_smoked:
33
+ @forced_validator
34
+ @classmethod
35
+ def validate_pack_years_smoked_cr(cls, data, info) -> None:
36
+ tobacco_smoking_status = data.get("tobacco_smoking_status", None)
37
+ pack_years_smoked = data.get("pack_years_smoked", None)
38
+
39
+ if tobacco_smoking_status in ["Never Smoker", "Unknown", "Not reported"] and pack_years_smoked:
36
40
  raise ValueLocError(
37
41
  "If tobacco_smoking_status indicates non-smoker, please leave pack_years_smoked blank.",
38
42
  loc="pack_years_smoked",
39
43
  )
40
- return self
@@ -1,12 +1,12 @@
1
- from typing import Self
2
-
3
- from pydantic import NonPositiveInt, model_validator
1
+ from pydantic import NonPositiveInt
2
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
3
 
5
4
  from cidc_api.models.errors import ValueLocError
6
5
  from cidc_api.models.pydantic.base import Base
7
6
  from cidc_api.models.types import UberonAnatomicalTerm, ICDO3MorphologicalCode, ICDO3MorphologicalTerm, MalignancyStatus
8
7
 
9
8
 
9
+ @forced_validators
10
10
  class OtherMalignancy(Base):
11
11
  __data_category__ = "other_malignancy"
12
12
  __cardinality__ = "many"
@@ -37,15 +37,19 @@ class OtherMalignancy(Base):
37
37
  # Indicates the participant’s current clinical state regarding the cancer diagnosis.
38
38
  other_malignancy_status: MalignancyStatus | None = None
39
39
 
40
- @model_validator(mode="after")
41
- def validate_code_or_term_or_description_cr(self) -> Self:
40
+ @forced_validator
41
+ @classmethod
42
+ def validate_code_or_term_or_description_cr(cls, data, info) -> None:
43
+ other_malignancy_morphological_term = data.get("other_malignancy_morphological_term", None)
44
+ other_malignancy_description = data.get("other_malignancy_description", None)
45
+ other_malignancy_morphological_code = data.get("other_malignancy_morphological_code", None)
46
+
42
47
  if (
43
- not self.other_malignancy_morphological_code
44
- and not self.other_malignancy_morphological_term
45
- and not self.other_malignancy_description
48
+ not other_malignancy_morphological_code
49
+ and not other_malignancy_morphological_term
50
+ and not other_malignancy_description
46
51
  ):
47
52
  raise ValueLocError(
48
53
  'Please provide at least one of "morphological_code", "morphological_term" or "malignancy_description".',
49
- loc="morphological_code",
54
+ loc="other_malignancy_morphological_code",
50
55
  )
51
- return self
@@ -1,6 +1,4 @@
1
- from typing import Self
2
-
3
- from pydantic import model_validator
1
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
2
 
5
3
  from cidc_api.models.errors import ValueLocError
6
4
  from cidc_api.models.pydantic.base import Base
@@ -8,6 +6,7 @@ from cidc_api.models.types import YNU
8
6
  from cidc_api.models.types import OffStudyReason
9
7
 
10
8
 
9
+ @forced_validators
11
10
  class Participant(Base):
12
11
  __data_category__ = "participant"
13
12
  __cardinality__ = "one"
@@ -39,20 +38,26 @@ class Participant(Base):
39
38
  # Additional information if "Other" is selected for off_study_reason. e.g. "Transfer to another study"
40
39
  off_study_reason_other: str | None = None
41
40
 
42
- @model_validator(mode="after")
43
- def off_study_reason_cr(self) -> Self:
44
- if self.off_study == "Yes" and not self.off_study_reason:
41
+ @forced_validator
42
+ @classmethod
43
+ def off_study_reason_cr(cls, data, info) -> None:
44
+ off_study = data.get("off_study", None)
45
+ off_study_reason = data.get("off_study_reason", None)
46
+
47
+ if off_study == "Yes" and not off_study_reason:
45
48
  raise ValueLocError(
46
49
  'If "off_study" is "Yes" then "off_study_reason" is required.',
47
50
  loc="off_study_reason",
48
51
  )
49
- return self
50
52
 
51
- @model_validator(mode="after")
52
- def off_study_reason_other_cr(self) -> Self:
53
- if self.off_study_reason == "Other" and not self.off_study_reason_other:
53
+ @forced_validator
54
+ @classmethod
55
+ def off_study_reason_other_cr(cls, data, info) -> None:
56
+ off_study_reason_other = data.get("off_study_reason_other", None)
57
+ off_study_reason = data.get("off_study_reason", None)
58
+
59
+ if off_study_reason == "Other" and not off_study_reason_other:
54
60
  raise ValueLocError(
55
61
  'If "off_study_reason" is "Other" then "off_study_reason_other" is required.',
56
62
  loc="off_study_reason_other",
57
63
  )
58
- return self
@@ -1,6 +1,5 @@
1
- from typing import Self
2
-
3
- from pydantic import NonNegativeInt, NonNegativeFloat, model_validator
1
+ from pydantic import NonNegativeInt, NonNegativeFloat
2
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
3
 
5
4
  from cidc_api.models.errors import ValueLocError
6
5
  from cidc_api.models.pydantic.base import Base
@@ -13,6 +12,7 @@ from cidc_api.models.types import (
13
12
  )
14
13
 
15
14
 
15
+ @forced_validators
16
16
  class RadiotherapyDose(Base):
17
17
  __data_category__ = "radiotherapy_dose"
18
18
  __cardinality__ = "many"
@@ -67,20 +67,26 @@ class RadiotherapyDose(Base):
67
67
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=7063755%20and%20ver_nr=1
68
68
  radiation_extent: RadiationExtent
69
69
 
70
- @model_validator(mode="after")
71
- def validate_changes_delays_description_cr(self) -> Self:
72
- if self.dose_changes_delays == "Yes" and not self.changes_delays_description:
70
+ @forced_validator
71
+ @classmethod
72
+ def validate_changes_delays_description_cr(cls, data, info) -> None:
73
+ dose_changes_delays = data.get("dose_changes_delays", None)
74
+ changes_delays_description = data.get("changes_delays_description", None)
75
+
76
+ if dose_changes_delays == "Yes" and not changes_delays_description:
73
77
  raise ValueLocError(
74
78
  'If dose_changes_delays is "Yes", please provide changes_delays_description.',
75
79
  loc="changes_delays_description",
76
80
  )
77
- return self
78
81
 
79
- @model_validator(mode="after")
80
- def validate_planned_dose_units_cr(self) -> Self:
81
- if self.planned_dose and not self.planned_dose_units:
82
+ @forced_validator
83
+ @classmethod
84
+ def validate_planned_dose_units_cr(cls, data, info) -> None:
85
+ planned_dose = data.get("planned_dose", None)
86
+ planned_dose_units = data.get("planned_dose_units", None)
87
+
88
+ if planned_dose and not planned_dose_units:
82
89
  raise ValueLocError(
83
90
  "If planned_dose is provided, please provide planned_dose_units.",
84
91
  loc="planned_dose_units",
85
92
  )
86
- return self
@@ -1,12 +1,12 @@
1
- from typing import Self
2
-
3
- from pydantic import NonNegativeInt, model_validator
1
+ from pydantic import NonNegativeInt
2
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
3
 
5
4
  from cidc_api.models.errors import ValueLocError
6
5
  from cidc_api.models.pydantic.base import Base
7
6
  from cidc_api.models.types import SurvivalStatus, YNUNA, CauseOfDeath
8
7
 
9
8
 
9
+ @forced_validators
10
10
  class Response(Base):
11
11
  __data_category__ = "response"
12
12
  __cardinality__ = "one"
@@ -47,29 +47,38 @@ class Response(Base):
47
47
  # Days from enrollment date to the last time the patient's vital status was verified.
48
48
  days_to_last_vital_status: NonNegativeInt | None = None
49
49
 
50
- @model_validator(mode="after")
51
- def validate_cause_of_death_cr(self) -> Self:
52
- if self.survival_status == "Dead" and not self.cause_of_death:
50
+ @forced_validator
51
+ @classmethod
52
+ def validate_cause_of_death_cr(cls, data, info) -> None:
53
+ survival_status = data.get("survival_status", None)
54
+ cause_of_death = data.get("cause_of_death", None)
55
+
56
+ if survival_status == "Dead" and not cause_of_death:
53
57
  raise ValueLocError(
54
58
  'If survival_status is "Dead" then cause_of_death is required.',
55
59
  loc="cause_of_death",
56
60
  )
57
- return self
58
61
 
59
- @model_validator(mode="after")
60
- def validate_cause_of_death_cr2(self) -> Self:
61
- if self.survival_status == "Alive" and self.cause_of_death:
62
+ @forced_validator
63
+ @classmethod
64
+ def validate_cause_of_death_cr2(cls, data, info) -> None:
65
+ survival_status = data.get("survival_status", None)
66
+ cause_of_death = data.get("cause_of_death", None)
67
+
68
+ if survival_status == "Alive" and cause_of_death:
62
69
  raise ValueLocError(
63
70
  'If survival_status is "Alive", please leave cause_of_death blank.',
64
71
  loc="cause_of_death",
65
72
  )
66
- return self
67
73
 
68
- @model_validator(mode="after")
69
- def validate_days_to_death_cr(self) -> Self:
70
- if self.survival_status in ["Alive", "Unknown"] and self.days_to_death:
74
+ @forced_validator
75
+ @classmethod
76
+ def validate_days_to_death_cr(cls, data, info) -> None:
77
+ survival_status = data.get("survival_status", None)
78
+ days_to_death = data.get("days_to_death", None)
79
+
80
+ if survival_status in ["Alive", "Unknown"] and days_to_death:
71
81
  raise ValueLocError(
72
82
  "If survival_status does not indicate death, please leave days_to_death blank.",
73
83
  loc="days_to_death",
74
84
  )
75
- return self
@@ -1,6 +1,7 @@
1
1
  from typing import Self
2
2
 
3
- from pydantic import PositiveInt, model_validator, NonNegativeInt
3
+ from pydantic import PositiveInt, NonNegativeInt, model_validator
4
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
5
 
5
6
  from cidc_api.models.errors import ValueLocError
6
7
  from cidc_api.models.pydantic.base import Base
@@ -19,6 +20,7 @@ negative_response_values = [
19
20
  ]
20
21
 
21
22
 
23
+ @forced_validators
22
24
  class ResponseBySystem(Base):
23
25
  __data_category__ = "response_by_system"
24
26
  __cardinality__ = "many"
@@ -71,90 +73,117 @@ class ResponseBySystem(Base):
71
73
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=5143957%20and%20ver_nr=1
72
74
  progression_free_survival: PositiveInt | None = None
73
75
 
74
- @model_validator(mode="after")
75
- def validate_response_duration_cr(self) -> Self:
76
- if self.best_overall_response in negative_response_values and self.response_duration:
76
+ @forced_validator
77
+ @classmethod
78
+ def validate_response_duration_cr(cls, data, info) -> None:
79
+ best_overall_response = data.get("best_overall_response", None)
80
+ response_duration = data.get("response_duration", None)
81
+
82
+ if best_overall_response in negative_response_values and response_duration:
77
83
  raise ValueLocError(
78
84
  "If best_overall_response does not indicate a positive response, "
79
85
  "please leave response_duration blank.",
80
86
  loc="response_duration",
81
87
  )
82
- return self
83
88
 
84
- @model_validator(mode="after")
85
- def validate_days_to_first_response_cr(self) -> Self:
86
- if self.best_overall_response in negative_response_values and self.days_to_first_response:
89
+ @forced_validator
90
+ @classmethod
91
+ def validate_days_to_first_response_cr(cls, data, info) -> None:
92
+ best_overall_response = data.get("best_overall_response", None)
93
+ days_to_first_response = data.get("days_to_first_response", None)
94
+
95
+ if best_overall_response in negative_response_values and days_to_first_response:
87
96
  raise ValueLocError(
88
97
  "If best_overall_response does not indicate a positive response, "
89
98
  "please leave days_to_first_response blank.",
90
99
  loc="days_to_first_response",
91
100
  )
92
- return self
93
101
 
94
- @model_validator(mode="after")
95
- def validate_days_to_best_response_cr(self) -> Self:
96
- if self.best_overall_response in negative_response_values and self.days_to_best_response:
102
+ @forced_validator
103
+ @classmethod
104
+ def validate_days_to_best_response_cr(cls, data, info) -> None:
105
+ best_overall_response = data.get("best_overall_response", None)
106
+ days_to_best_response = data.get("days_to_best_response", None)
107
+
108
+ if best_overall_response in negative_response_values and days_to_best_response:
97
109
  raise ValueLocError(
98
- "If best_overall_response does not indicate a positive response, \
99
- please leave days_to_best_response blank.",
110
+ "If best_overall_response does not indicate a positive response, "
111
+ "please leave days_to_best_response blank.",
100
112
  loc="days_to_best_response",
101
113
  )
102
- return self
103
114
 
104
- @model_validator(mode="after")
105
- def validate_days_to_disease_progression_cr(self) -> Self:
106
- if self.progression in ["No", "Unknown", "Not Applicable"] and self.days_to_disease_progression:
115
+ @forced_validator
116
+ @classmethod
117
+ def validate_days_to_disease_progression_cr(cls, data, info) -> None:
118
+ progression = data.get("progression", None)
119
+ days_to_disease_progression = data.get("days_to_disease_progression", None)
120
+
121
+ if progression in ["No", "Unknown", "Not Applicable"] and days_to_disease_progression:
107
122
  raise ValueLocError(
108
- "If progression does not indicate confirmed progression of the disease, \
109
- please leave days_to_disease_progress blank.",
110
- loc="days_to_disease_progress",
123
+ "If progression does not indicate confirmed progression of the disease, "
124
+ "please leave days_to_disease_progression blank.",
125
+ loc="days_to_disease_progression",
111
126
  )
112
- return self
113
127
 
114
- @model_validator(mode="after")
115
- def validate_progression_free_survival_cr(self) -> Self:
116
- if self.progression_free_survival_event in ["Unknown", "Not Applicable"] and self.progression_free_survival:
128
+ @forced_validator
129
+ @classmethod
130
+ def validate_progression_free_survival_cr(cls, data, info) -> None:
131
+ progression_free_survival_event = data.get("progression_free_survival_event", None)
132
+ progression_free_survival = data.get("progression_free_survival", None)
133
+
134
+ if progression_free_survival_event in ["Unknown", "Not Applicable"] and progression_free_survival:
117
135
  raise ValueLocError(
118
- "If progression_free_survival_event is not known, \
119
- please leave progression_free_survival blank.",
136
+ "If progression_free_survival_event is not known, " "please leave progression_free_survival blank.",
120
137
  loc="progression_free_survival",
121
138
  )
122
- return self
123
139
 
124
- @model_validator(mode="after")
125
- def validate_days_to_best_response_chronology(self) -> Self:
126
- if self.days_to_best_response is not None and self.days_to_first_response is not None:
127
- if self.days_to_best_response < self.days_to_first_response:
140
+ @forced_validator
141
+ @classmethod
142
+ def validate_days_to_best_response_chronology(cls, data, info) -> None:
143
+ days_to_first_response = data.get("days_to_first_response", None)
144
+ days_to_best_response = data.get("days_to_best_response", None)
145
+
146
+ if days_to_best_response is not None and days_to_first_response is not None:
147
+ if int(days_to_best_response) < int(days_to_first_response):
128
148
  raise ValueLocError(
129
149
  'Violate "days_to_best_response" >= days_to_first_response"',
130
150
  loc="days_to_best_response",
131
151
  )
132
- return self
133
152
 
134
- @model_validator(mode="after")
135
- def validate_days_to_disease_progression_chronology(self) -> Self:
136
- if self.days_to_first_response is not None and self.days_to_disease_progression is not None:
137
- if self.days_to_first_response >= self.days_to_disease_progression:
153
+ @forced_validator
154
+ @classmethod
155
+ def validate_days_to_disease_progression_chronology(cls, data, info) -> None:
156
+ days_to_disease_progression = data.get("days_to_disease_progression", None)
157
+ days_to_first_response = data.get("days_to_first_response", None)
158
+
159
+ if days_to_first_response is not None and days_to_disease_progression is not None:
160
+ if int(days_to_first_response) >= int(days_to_disease_progression):
138
161
  raise ValueLocError(
139
162
  'Violate "days_to_first_response" < "days_to_disease_progression"',
140
163
  loc="days_to_first_response",
141
164
  )
142
- return self
143
165
 
144
- @model_validator(mode="after")
145
- def validate_days_to_best_response_progression_chronology(self) -> Self:
146
- if self.days_to_best_response is not None and self.days_to_disease_progression is not None:
147
- if self.days_to_best_response >= self.days_to_disease_progression:
166
+ @forced_validator
167
+ @classmethod
168
+ def validate_days_to_best_response_progression_chronology(cls, data, info) -> None:
169
+ days_to_disease_progression = data.get("days_to_disease_progression", None)
170
+ days_to_best_response = data.get("days_to_best_response", None)
171
+
172
+ if days_to_best_response is not None and days_to_disease_progression is not None:
173
+ if int(days_to_best_response) >= int(days_to_disease_progression):
148
174
  raise ValueLocError(
149
175
  'Violate "days_to_best_response" < "days_to_disease_progression"',
150
176
  loc="days_to_best_response",
151
177
  )
152
- return self
153
178
 
154
179
  @model_validator(mode="after")
155
180
  def validate_days_to_last_vital_status_chronology(self) -> Self:
156
- if not self.response or not self.response.days_to_last_vital_status:
181
+ if not self.response:
182
+ return self
183
+
184
+ if not self.response.days_to_last_vital_status:
157
185
  return self
186
+
158
187
  max_value = max(
159
188
  self.response.days_to_last_vital_status or 0,
160
189
  self.days_to_first_response or 0,
@@ -171,8 +200,11 @@ class ResponseBySystem(Base):
171
200
 
172
201
  @model_validator(mode="after")
173
202
  def validate_days_to_death_chronology(self) -> Self:
174
- if not self.response or not self.response.days_to_death:
203
+ if not self.response:
175
204
  return self
205
+ if not self.response.days_to_death:
206
+ return self
207
+
176
208
  max_value = max(
177
209
  self.response.days_to_death or 0,
178
210
  self.days_to_first_response or 0,
@@ -185,3 +217,4 @@ class ResponseBySystem(Base):
185
217
  ">= max(days_to_first_response,days_to_best_response,days_to_disease_progression)",
186
218
  loc="days_to_death,days_to_first_response,days_to_best_response,days_to_disease_progression",
187
219
  )
220
+ return self
@@ -1,12 +1,13 @@
1
- from typing import Self
1
+ from pydantic import NonNegativeInt
2
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
2
3
 
3
- from pydantic import NonNegativeInt, model_validator
4
4
 
5
5
  from cidc_api.models.errors import ValueLocError
6
6
  from cidc_api.models.pydantic.base import Base
7
7
  from cidc_api.models.types import SurgicalProcedure, UberonAnatomicalTerm, YNU
8
8
 
9
9
 
10
+ @forced_validators
10
11
  class Surgery(Base):
11
12
  __data_category__ = "surgery"
12
13
  __cardinality__ = "many"
@@ -43,11 +44,14 @@ class Surgery(Base):
43
44
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=13362284%20and%20ver_nr=1
44
45
  extent_of_residual_disease: str | None = None
45
46
 
46
- @model_validator(mode="after")
47
- def validate_procedure_other_cr(self) -> Self:
48
- if self.procedure == "Other, specify" and not self.procedure_other:
47
+ @forced_validator
48
+ @classmethod
49
+ def validate_procedure_other_cr(cls, data, info) -> None:
50
+ procedure = data.get("procedure", None)
51
+ procedure_other = data.get("procedure_other", None)
52
+
53
+ if procedure == "Other, specify" and not procedure_other:
49
54
  raise ValueLocError(
50
55
  'If procedure is "Other, specify", please provide procedure_other.',
51
56
  loc="procedure_other",
52
57
  )
53
- return self
@@ -1,12 +1,12 @@
1
- from typing import Self
2
-
3
- from pydantic import NonNegativeInt, NonNegativeFloat, PositiveFloat, model_validator
1
+ from pydantic import NonNegativeInt, NonNegativeFloat, PositiveFloat
2
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
3
 
5
4
  from cidc_api.models.errors import ValueLocError
6
5
  from cidc_api.models.pydantic.base import Base
7
6
  from cidc_api.models.types import YNU, TherapyAgentDoseUnits
8
7
 
9
8
 
9
+ @forced_validators
10
10
  class TherapyAgentDose(Base):
11
11
  __data_category__ = "therapy_agent_dose"
12
12
  __cardinality__ = "many"
@@ -55,20 +55,26 @@ class TherapyAgentDose(Base):
55
55
  # Description of the dose changes, misses, or delays.
56
56
  changes_delays_description: str | None = None
57
57
 
58
- @model_validator(mode="after")
59
- def validate_changes_delays_description_cr(self) -> Self:
60
- if self.dose_changes_delays == "Yes" and not self.changes_delays_description:
58
+ @forced_validator
59
+ @classmethod
60
+ def validate_changes_delays_description_cr(cls, data, info) -> None:
61
+ dose_changes_delays = data.get("dose_changes_delays", None)
62
+ changes_delays_description = data.get("changes_delays_description", None)
63
+
64
+ if dose_changes_delays == "Yes" and not changes_delays_description:
61
65
  raise ValueLocError(
62
66
  'If dose_changes_delays is "Yes", please provide changes_delays_description.',
63
67
  loc="changes_delays_description",
64
68
  )
65
- return self
66
69
 
67
- @model_validator(mode="after")
68
- def validate_planned_dose_units_cr(self) -> Self:
69
- if self.planned_dose and not self.planned_dose_units:
70
+ @forced_validator
71
+ @classmethod
72
+ def validate_planned_dose_units_cr(cls, data, info) -> None:
73
+ planned_dose = data.get("planned_dose", None)
74
+ planned_dose_units = data.get("planned_dose_units", None)
75
+
76
+ if planned_dose and not planned_dose_units:
70
77
  raise ValueLocError(
71
78
  "If planned_dose is provided, please provide planned_dose_units.",
72
79
  loc="planned_dose_units",
73
80
  )
74
- return self
@@ -1,12 +1,11 @@
1
- from typing import Self
2
-
3
- from pydantic import model_validator
1
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
4
2
 
5
3
  from cidc_api.models.errors import ValueLocError
6
4
  from cidc_api.models.pydantic.base import Base
7
5
  from cidc_api.models.types import YNU, OffTreatmentReason
8
6
 
9
7
 
8
+ @forced_validators
10
9
  class Treatment(Base):
11
10
  __data_category__ = "treatment"
12
11
  __cardinality__ = "many"
@@ -38,20 +37,28 @@ class Treatment(Base):
38
37
  # If "Other" is selected for "off_treatment_reason", provide a description of the reason.
39
38
  off_treatment_reason_other: str | None = None
40
39
 
41
- @model_validator(mode="after")
42
- def validate_off_treatment_reason_cr(self) -> Self:
43
- if self.off_treatment == "Yes" and not self.off_treatment_reason:
40
+ @forced_validator
41
+ @classmethod
42
+ def validate_off_treatment_reason_cr(cls, data, info) -> None:
43
+ off_treatment = data.get("off_treatment", None)
44
+ off_treatment_reason = data.get("off_treatment_reason", None)
45
+
46
+ if off_treatment == "Yes" and not off_treatment_reason:
44
47
  raise ValueLocError(
45
48
  'If off_treatment is "Yes", please provide off_treatment_reason.',
46
49
  loc="off_treatment_reason",
47
50
  )
48
- return self
51
+ return off_treatment_reason
52
+
53
+ @forced_validator
54
+ @classmethod
55
+ def validate_off_treatment_reason_other_cr(cls, data, info) -> None:
56
+ off_treatment_reason = data.get("off_treatment_reason", None)
57
+ off_treatment_reason_other = data.get("off_treatment_reason_other", None)
49
58
 
50
- @model_validator(mode="after")
51
- def validate_off_treatment_reason_other_cr(self) -> Self:
52
- if self.off_treatment_reason == "Other" and not self.off_treatment_reason_other:
59
+ if off_treatment_reason == "Other" and not off_treatment_reason_other:
53
60
  raise ValueLocError(
54
61
  'If off_treatment_reason is "Other", please provide off_treatment_reason_other.',
55
62
  loc="off_treatment_reason_other",
56
63
  )
57
- return self
64
+ return off_treatment_reason_other
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nci_cidc_api_modules
3
- Version: 1.2.51
3
+ Version: 1.2.52
4
4
  Summary: SQLAlchemy data models and configuration tools used in the NCI CIDC API
5
5
  Home-page: https://github.com/NCI-CIDC/cidc-api-gae
6
6
  License: MIT license
@@ -1,5 +1,5 @@
1
1
  boot.py,sha256=oS_16OgU34iDxyFNpJl5rb5KbfbsCXroUCNLwVPxPNg,416
2
- cidc_api/__init__.py,sha256=kgsz9u_lLDc3N0akch6v9PpMXz_PW7_aEHXRb1pWgHg,23
2
+ cidc_api/__init__.py,sha256=jf5CtcUq4OTsCQcG2e0HUKeI9KD06OUpOQioBuO1h-Y,23
3
3
  cidc_api/telemetry.py,sha256=LuZWkG8CKCn23O41RTNxEOQwMmfpp-fQ6zSInZhVJg8,3333
4
4
  cidc_api/config/__init__.py,sha256=5mX8GAPxUKV84iS-aGOoE-4m68LsOCGCDptXNdlgvj0,148
5
5
  cidc_api/config/db.py,sha256=mEz8ugjvRNGylCqDYHaaMqaZfDh7sbd76BowmfBvq5c,2428
@@ -81,32 +81,32 @@ cidc_api/models/db/stage2/trial_orm.py,sha256=7_5kA1Yv80GFQykK8AIlFC0vtrsb2H8xLs
81
81
  cidc_api/models/files/__init__.py,sha256=8BMTnUSHzUbz0lBeEQY6NvApxDD3GMWMduoVMos2g4Y,213
82
82
  cidc_api/models/files/details.py,sha256=sZkGM7iEV4-J6IDQCdiMV6KBDLbPxCOqUMaU3aY9rX8,65153
83
83
  cidc_api/models/files/facets.py,sha256=vieZ3z_tYB29NqtvNzvKqajVwxM5ZLLZKeyQTq5lheU,33776
84
- cidc_api/models/pydantic/base.py,sha256=6Lsf4fekIS1E-DwZmgCXlfm3Qq9_23dA_v3iz1w2JoA,1427
84
+ cidc_api/models/pydantic/base.py,sha256=sL_0PwEH2vS8BMT_w-zL7CPeaU-R7ih5afRigX6k2DM,4069
85
85
  cidc_api/models/pydantic/stage1/__init__.py,sha256=H-NDjCd8isHTgZ7sqz-MIbzC-PpW1kEtR-x-Dyc8r6Q,1667
86
86
  cidc_api/models/pydantic/stage1/additional_treatment.py,sha256=TFVzjd1NPLSfH9UnL2W3aZhpZSzDsg97j9IK8t882YA,1050
87
- cidc_api/models/pydantic/stage1/adverse_event.py,sha256=YzH8YbgxvjrrcJENtbctYCziR_io4fKPtSiHxO0GV9A,4652
87
+ cidc_api/models/pydantic/stage1/adverse_event.py,sha256=ZW5iVnMmX7NtZcsgDAjzPorWPZDSJSmTXGK1EsU9AaQ,5167
88
88
  cidc_api/models/pydantic/stage1/baseline_clinical_assessment.py,sha256=5FYvtOVf_nIK1h3xTs3vQgJe2ivQSVHt_wfrTSDgVxU,1119
89
- cidc_api/models/pydantic/stage1/comorbidity.py,sha256=K9MyK4XsSGr-3TjrDH_GJN1rG3uWqkyECbwGGuqkbzc,1528
89
+ cidc_api/models/pydantic/stage1/comorbidity.py,sha256=IfxFb0dZAdCY--Ze5kiZ3CrzVKJtuknpcpvrQ3ki-gI,1769
90
90
  cidc_api/models/pydantic/stage1/consent_group.py,sha256=aP_nd0GKK6K7VH-6IZRSDDX2RxFM_j9B36FIRGwysOU,1216
91
- cidc_api/models/pydantic/stage1/demographic.py,sha256=V5Fymdl2ge6eGcnCnP7ekCJuzfR7XeCgwclBKCMeolE,6304
92
- cidc_api/models/pydantic/stage1/disease.py,sha256=e-BT6Z_QMyDJhtzJcYbMdqABkuVsX4OWw5HGnNuLzZM,8212
93
- cidc_api/models/pydantic/stage1/exposure.py,sha256=XtnLxXSBrvuvOgrdg2A-x_FclQvT8zJkGeXIB50ZJs0,1426
91
+ cidc_api/models/pydantic/stage1/demographic.py,sha256=S2riNy6njMSamgupFAsbM656TDamQExRzUGKNYaBaOo,6752
92
+ cidc_api/models/pydantic/stage1/disease.py,sha256=G8d2iqsMhmDA3uxjOitxzmlFKIRxyUMp-o-H7eUfZFg,9250
93
+ cidc_api/models/pydantic/stage1/exposure.py,sha256=bd9gUmOQ7ge-xvoxD17pX95MKxfHKD2xNaFEJAMAkn4,1571
94
94
  cidc_api/models/pydantic/stage1/gvhd_diagnosis_acute.py,sha256=jVMoWuLfspOZtOpC7rg1EzdvEAg8qEabcTaS6RRobZ8,1395
95
95
  cidc_api/models/pydantic/stage1/gvhd_diagnosis_chronic.py,sha256=SRwkAL07YyIZM4bYCqm01aKqSvj3yRmpsqFKxAZEZoM,1382
96
96
  cidc_api/models/pydantic/stage1/gvhd_organ_acute.py,sha256=VxlNGRJYjm9tW2lyLll8pD0t9JZs18gicnH-Ci_nR2I,771
97
97
  cidc_api/models/pydantic/stage1/gvhd_organ_chronic.py,sha256=Fnp1Qm-ZDDtDLrgOAnEOUnQVNWI9WrTSomiFPStGmjc,810
98
- cidc_api/models/pydantic/stage1/medical_history.py,sha256=5QU01b5-lKIWiz7GoIwZmZ_meFJ70ku7tvd3To1OpIA,1821
99
- cidc_api/models/pydantic/stage1/other_malignancy.py,sha256=wIiYa3EffLYA4hu8JJORCZwG2GRbRdl2y47VJN4OffI,2283
100
- cidc_api/models/pydantic/stage1/participant.py,sha256=QQVi0jhqSot1TiopongU_VnJHvk2RLKUpKtR15a_A1M,2402
98
+ cidc_api/models/pydantic/stage1/medical_history.py,sha256=z-cE_ChyWUKTvWjVhKMUAYvap9YfpZn6Z2BiHhBXx6g,2000
99
+ cidc_api/models/pydantic/stage1/other_malignancy.py,sha256=VIzhbkauaDfZdPxhTYpqNtwYBxE_4osE9wyZ67LQJx4,2622
100
+ cidc_api/models/pydantic/stage1/participant.py,sha256=QfRkK2okY7nM84y9QMK12kZLURBGk-HNWWHL4pQZjec,2655
101
101
  cidc_api/models/pydantic/stage1/prior_treatment.py,sha256=axhyPHwYCoIvcIonjyPJ95HUyaAXqIuOd6hhoDM3LJU,2101
102
- cidc_api/models/pydantic/stage1/radiotherapy_dose.py,sha256=eGCkqfK2jIu3Lh0G51hXs4oqP9CRAxJrSbApdNjANb8,3536
103
- cidc_api/models/pydantic/stage1/response.py,sha256=3Cuhu0DBew96e-k0sSN4vvPJ1xSoejXJdjpXw9HPtjo,3126
104
- cidc_api/models/pydantic/stage1/response_by_system.py,sha256=3Om1R2vJd6C7UDQAIh-suDnwrZGquFf9SpirPJDKFUs,8590
102
+ cidc_api/models/pydantic/stage1/radiotherapy_dose.py,sha256=gDaL1OQuSuTauml6HafBiVvBn8gcynS3G1fa9KNhEPA,3833
103
+ cidc_api/models/pydantic/stage1/response.py,sha256=TJlKmUz9SypbbDnv6iSmLb4KPEuFUPe9Q8iNSGsLbfI,3491
104
+ cidc_api/models/pydantic/stage1/response_by_system.py,sha256=gjyo_jDyc2xHNNrUwoFqH3Fz51v2eSr3JRHkH0xwVqI,9851
105
105
  cidc_api/models/pydantic/stage1/specimen.py,sha256=scDekZ-RtXOQTTLburhqT3RF4KM34iY9NAV1wYi_HSg,1281
106
106
  cidc_api/models/pydantic/stage1/stem_cell_transplant.py,sha256=XsDVUksqbIprPxHwLlwHGyji4jsIFNMcIk-S0H4rDnQ,1228
107
- cidc_api/models/pydantic/stage1/surgery.py,sha256=IPBhgyPHuPrPpy0Qs9j2rHO7ig7J8ixBVNp_1l5TvzU,2271
108
- cidc_api/models/pydantic/stage1/therapy_agent_dose.py,sha256=ZIsImguwBuAFGogla1rBN6N_FNUzhoC3OkconsJ-33k,3230
109
- cidc_api/models/pydantic/stage1/treatment.py,sha256=C2xQ_fU0ZxGgBzes7hyHOHvicAzSjyva3Z5F78tor30,2244
107
+ cidc_api/models/pydantic/stage1/surgery.py,sha256=sHPzYMNusYIQrC6gjWopwxo0j9BNOq_Tcl6HKAMvuAQ,2421
108
+ cidc_api/models/pydantic/stage1/therapy_agent_dose.py,sha256=XX7JM4CdvTuAsbI5rI4wYDcbevP46BetTk_z7N_EL-M,3527
109
+ cidc_api/models/pydantic/stage1/treatment.py,sha256=eEjCcmkLdPmS2kg9dtUm3pt9apo1sRypBbte_xUVAYU,2607
110
110
  cidc_api/models/pydantic/stage1/trial.py,sha256=EFsMVMxxHbPeaBb6JG9JpX1qjtoho2t-GZnaI0gsYrY,1859
111
111
  cidc_api/models/pydantic/stage2/__init__.py,sha256=OQRG5wFkNsyAU_0KI1W5PDn8pBDe1EJDcgKZ19shh_g,2331
112
112
  cidc_api/models/pydantic/stage2/additional_treatment.py,sha256=TFVzjd1NPLSfH9UnL2W3aZhpZSzDsg97j9IK8t882YA,1050
@@ -160,8 +160,8 @@ cidc_api/shared/gcloud_client.py,sha256=EJ2ZzZ0gwDOZx18PEzXil4F-jpruacShkVIoecw8
160
160
  cidc_api/shared/jose.py,sha256=-qzGzEDAlokEp9E7WtBtQkXyyfPWTYXlwYpCqVJWmqM,1830
161
161
  cidc_api/shared/rest_utils.py,sha256=RwR30WOUAYCxL7V-i2totEyeriG30GbBDvBcpLXhM9w,6594
162
162
  cidc_api/shared/utils.py,sha256=-gLnzxCR9E6h0plt2xrNisUG5_Y6GhhVwz3DgDIzpvs,367
163
- nci_cidc_api_modules-1.2.51.dist-info/licenses/LICENSE,sha256=pNYWVTHaYonnmJyplmeAp7tQAjosmDpAWjb34jjv7Xs,1102
164
- nci_cidc_api_modules-1.2.51.dist-info/METADATA,sha256=MCveB10mn5WAmlZX44mM0Z0BCCCu2wsttqjPitAF4Nc,40301
165
- nci_cidc_api_modules-1.2.51.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
166
- nci_cidc_api_modules-1.2.51.dist-info/top_level.txt,sha256=BSJqF6ura8-bWCKZvarvQEKvidMM05lH0bLQsNOrI0o,14
167
- nci_cidc_api_modules-1.2.51.dist-info/RECORD,,
163
+ nci_cidc_api_modules-1.2.52.dist-info/licenses/LICENSE,sha256=pNYWVTHaYonnmJyplmeAp7tQAjosmDpAWjb34jjv7Xs,1102
164
+ nci_cidc_api_modules-1.2.52.dist-info/METADATA,sha256=LmysKc5tWnhHucOWIA2ZesTGvnWy63VG7a6zw8bRtXQ,40301
165
+ nci_cidc_api_modules-1.2.52.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
166
+ nci_cidc_api_modules-1.2.52.dist-info/top_level.txt,sha256=BSJqF6ura8-bWCKZvarvQEKvidMM05lH0bLQsNOrI0o,14
167
+ nci_cidc_api_modules-1.2.52.dist-info/RECORD,,