nci-cidc-api-modules 1.2.45__py3-none-any.whl → 1.2.53__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.
Files changed (27) hide show
  1. boot.py +14 -0
  2. cidc_api/__init__.py +1 -1
  3. cidc_api/config/settings.py +4 -10
  4. cidc_api/models/errors.py +7 -0
  5. cidc_api/models/models.py +14 -2
  6. cidc_api/models/pydantic/base.py +63 -2
  7. cidc_api/models/pydantic/stage1/adverse_event.py +51 -24
  8. cidc_api/models/pydantic/stage1/comorbidity.py +15 -8
  9. cidc_api/models/pydantic/stage1/demographic.py +45 -28
  10. cidc_api/models/pydantic/stage1/disease.py +100 -58
  11. cidc_api/models/pydantic/stage1/exposure.py +14 -8
  12. cidc_api/models/pydantic/stage1/medical_history.py +15 -8
  13. cidc_api/models/pydantic/stage1/other_malignancy.py +17 -11
  14. cidc_api/models/pydantic/stage1/participant.py +26 -14
  15. cidc_api/models/pydantic/stage1/radiotherapy_dose.py +27 -14
  16. cidc_api/models/pydantic/stage1/response.py +41 -22
  17. cidc_api/models/pydantic/stage1/response_by_system.py +138 -30
  18. cidc_api/models/pydantic/stage1/surgery.py +15 -7
  19. cidc_api/models/pydantic/stage1/therapy_agent_dose.py +27 -14
  20. cidc_api/models/pydantic/stage1/treatment.py +28 -14
  21. cidc_api/shared/file_handling.py +2 -0
  22. cidc_api/shared/gcloud_client.py +20 -2
  23. {nci_cidc_api_modules-1.2.45.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/METADATA +13 -12
  24. {nci_cidc_api_modules-1.2.45.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/RECORD +27 -25
  25. {nci_cidc_api_modules-1.2.45.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/WHEEL +1 -1
  26. {nci_cidc_api_modules-1.2.45.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/top_level.txt +1 -0
  27. {nci_cidc_api_modules-1.2.45.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +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
 
3
+ from cidc_api.models.errors import ValueLocError
5
4
  from cidc_api.models.pydantic.base import Base
6
5
  from cidc_api.models.types import YNU, ExposureType
7
6
 
8
7
 
8
+ @forced_validators
9
9
  class Exposure(Base):
10
10
  __data_category__ = "exposure"
11
11
  __cardinality__ = "many"
@@ -25,8 +25,14 @@ class Exposure(Base):
25
25
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=15753203%20and%20ver_nr=1
26
26
  exposure_type: ExposureType | None = None
27
27
 
28
- @model_validator(mode="after")
29
- def validate_exposure_type_cr(self) -> Self:
30
- if self.carcinogen_exposure in ["No", "Unknown"] and self.exposure_type:
31
- raise ValueError("If carcinogen_exposure indicates non exposure, please leave exposure_type blank.")
32
- return self
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:
35
+ raise ValueLocError(
36
+ "If carcinogen_exposure indicates non exposure, please leave exposure_type blank.",
37
+ loc="exposure_type",
38
+ )
@@ -1,11 +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
 
4
+ from cidc_api.models.errors import ValueLocError
5
5
  from cidc_api.models.pydantic.base import Base
6
6
  from cidc_api.models.types import TobaccoSmokingStatus
7
7
 
8
8
 
9
+ @forced_validators
9
10
  class MedicalHistory(Base):
10
11
  __data_category__ = "medical_history"
11
12
  __cardinality__ = "one"
@@ -29,8 +30,14 @@ class MedicalHistory(Base):
29
30
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=16089302%20and%20ver_nr=1
30
31
  num_prior_systemic_therapies: NonNegativeInt | None = None
31
32
 
32
- @model_validator(mode="after")
33
- def validate_pack_years_smoked_cr(self) -> Self:
34
- if self.tobacco_smoking_status in ["Never Smoker", "Unknown", "Not reported"] and self.pack_years_smoked:
35
- raise ValueError("If tobacco_smoking_status indicates non-smoker, please leave pack_years_smoked blank.")
36
- return self
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:
40
+ raise ValueLocError(
41
+ "If tobacco_smoking_status indicates non-smoker, please leave pack_years_smoked blank.",
42
+ loc="pack_years_smoked",
43
+ )
@@ -1,11 +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
 
4
+ from cidc_api.models.errors import ValueLocError
5
5
  from cidc_api.models.pydantic.base import Base
6
6
  from cidc_api.models.types import UberonAnatomicalTerm, ICDO3MorphologicalCode, ICDO3MorphologicalTerm, MalignancyStatus
7
7
 
8
8
 
9
+ @forced_validators
9
10
  class OtherMalignancy(Base):
10
11
  __data_category__ = "other_malignancy"
11
12
  __cardinality__ = "many"
@@ -36,14 +37,19 @@ class OtherMalignancy(Base):
36
37
  # Indicates the participant’s current clinical state regarding the cancer diagnosis.
37
38
  other_malignancy_status: MalignancyStatus | None = None
38
39
 
39
- @model_validator(mode="after")
40
- 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
+
41
47
  if (
42
- not self.other_malignancy_morphological_code
43
- and not self.other_malignancy_morphological_term
44
- 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
45
51
  ):
46
- raise ValueError(
47
- 'Please provide at least one of "morphological_code", "morphological_term" or "malignancy_description".'
52
+ raise ValueLocError(
53
+ 'Please provide at least one of "morphological_code", "morphological_term" or "malignancy_description".',
54
+ loc="other_malignancy_morphological_code",
48
55
  )
49
- return self
@@ -1,12 +1,12 @@
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
 
3
+ from cidc_api.models.errors import ValueLocError
5
4
  from cidc_api.models.pydantic.base import Base
6
5
  from cidc_api.models.types import YNU
7
6
  from cidc_api.models.types import OffStudyReason
8
7
 
9
8
 
9
+ @forced_validators
10
10
  class Participant(Base):
11
11
  __data_category__ = "participant"
12
12
  __cardinality__ = "one"
@@ -38,14 +38,26 @@ class Participant(Base):
38
38
  # Additional information if "Other" is selected for off_study_reason. e.g. "Transfer to another study"
39
39
  off_study_reason_other: str | None = None
40
40
 
41
- @model_validator(mode="after")
42
- def off_study_reason_cr(self) -> Self:
43
- if self.off_study == "Yes" and not self.off_study_reason:
44
- raise ValueError('If "off_study" is "Yes" then "off_study_reason" is required.')
45
- return self
46
-
47
- @model_validator(mode="after")
48
- def off_study_reason_other_cr(self) -> Self:
49
- if self.off_study_reason == "Other" and not self.off_study_reason_other:
50
- raise ValueError('If "off_study_reason" is "Other" then "off_study_reason_other" is required.')
51
- return self
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:
48
+ raise ValueLocError(
49
+ 'If "off_study" is "Yes" then "off_study_reason" is required.',
50
+ loc="off_study_reason",
51
+ )
52
+
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:
60
+ raise ValueLocError(
61
+ 'If "off_study_reason" is "Other" then "off_study_reason_other" is required.',
62
+ loc="off_study_reason_other",
63
+ )
@@ -1,7 +1,7 @@
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
 
4
+ from cidc_api.models.errors import ValueLocError
5
5
  from cidc_api.models.pydantic.base import Base
6
6
  from cidc_api.models.types import (
7
7
  YNU,
@@ -12,6 +12,7 @@ from cidc_api.models.types import (
12
12
  )
13
13
 
14
14
 
15
+ @forced_validators
15
16
  class RadiotherapyDose(Base):
16
17
  __data_category__ = "radiotherapy_dose"
17
18
  __cardinality__ = "many"
@@ -66,14 +67,26 @@ class RadiotherapyDose(Base):
66
67
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=7063755%20and%20ver_nr=1
67
68
  radiation_extent: RadiationExtent
68
69
 
69
- @model_validator(mode="after")
70
- def validate_changes_delays_description_cr(self) -> Self:
71
- if self.dose_changes_delays == "Yes" and not self.changes_delays_description:
72
- raise ValueError('If dose_changes_delays is "Yes", please provide changes_delays_description.')
73
- return self
74
-
75
- @model_validator(mode="after")
76
- def validate_planned_dose_units_cr(self) -> Self:
77
- if self.planned_dose and not self.planned_dose_units:
78
- raise ValueError("If planned_dose is provided, please provide planned_dose_units.")
79
- return self
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:
77
+ raise ValueLocError(
78
+ 'If dose_changes_delays is "Yes", please provide changes_delays_description.',
79
+ loc="changes_delays_description",
80
+ )
81
+
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:
89
+ raise ValueLocError(
90
+ "If planned_dose is provided, please provide planned_dose_units.",
91
+ loc="planned_dose_units",
92
+ )
@@ -1,11 +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
 
4
+ from cidc_api.models.errors import ValueLocError
5
5
  from cidc_api.models.pydantic.base import Base
6
6
  from cidc_api.models.types import SurvivalStatus, YNUNA, CauseOfDeath
7
7
 
8
8
 
9
+ @forced_validators
9
10
  class Response(Base):
10
11
  __data_category__ = "response"
11
12
  __cardinality__ = "one"
@@ -44,22 +45,40 @@ class Response(Base):
44
45
  evaluable_for_efficacy: bool
45
46
 
46
47
  # Days from enrollment date to the last time the patient's vital status was verified.
47
- days_to_last_vital_status: NonNegativeInt | None = None # TODO: Needs CR check
48
-
49
- @model_validator(mode="after")
50
- def validate_cause_of_death_cr(self) -> Self:
51
- if self.survival_status == "Dead" and not self.cause_of_death:
52
- raise ValueError('If survival_status is "Dead" then cause_of_death is required.')
53
- return self
54
-
55
- @model_validator(mode="after")
56
- def validate_cause_of_death_cr2(self) -> Self:
57
- if self.survival_status == "Alive" and self.cause_of_death:
58
- raise ValueError('If survival_status is "Alive", please leave cause_of_death blank.')
59
- return self
60
-
61
- @model_validator(mode="after")
62
- def validate_days_to_death_cr(self) -> Self:
63
- if self.survival_status in ["Alive", "Unknown"] and self.days_to_death:
64
- raise ValueError("If survival_status does not indicate death, please leave days_to_death blank.")
65
- return self
48
+ days_to_last_vital_status: NonNegativeInt | None = None
49
+
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:
57
+ raise ValueLocError(
58
+ 'If survival_status is "Dead" then cause_of_death is required.',
59
+ loc="cause_of_death",
60
+ )
61
+
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:
69
+ raise ValueLocError(
70
+ 'If survival_status is "Alive", please leave cause_of_death blank.',
71
+ loc="cause_of_death",
72
+ )
73
+
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:
81
+ raise ValueLocError(
82
+ "If survival_status does not indicate death, please leave days_to_death blank.",
83
+ loc="days_to_death",
84
+ )
@@ -1,8 +1,11 @@
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
 
6
+ from cidc_api.models.errors import ValueLocError
5
7
  from cidc_api.models.pydantic.base import Base
8
+ from cidc_api.models.pydantic.stage1.response import Response
6
9
  from cidc_api.models.types import ResponseSystem, ResponseSystemVersion, BestOverallResponse, YNUNA
7
10
 
8
11
 
@@ -17,6 +20,7 @@ negative_response_values = [
17
20
  ]
18
21
 
19
22
 
23
+ @forced_validators
20
24
  class ResponseBySystem(Base):
21
25
  __data_category__ = "response_by_system"
22
26
  __cardinality__ = "many"
@@ -28,6 +32,9 @@ class ResponseBySystem(Base):
28
32
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=12220014%20and%20ver_nr=1
29
33
  participant_id: str | None = None
30
34
 
35
+ # The linked parent response for the participant. Used for cross-model validation.
36
+ response: Response | None = None
37
+
31
38
  # A standardized method used to evaluate and categorize the participant’s clinical response to treatment based on predefined criteria.
32
39
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=13381490%20and%20ver_nr=1
33
40
  response_system: ResponseSystem
@@ -66,47 +73,148 @@ class ResponseBySystem(Base):
66
73
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=5143957%20and%20ver_nr=1
67
74
  progression_free_survival: PositiveInt | None = None
68
75
 
69
- @model_validator(mode="after")
70
- def validate_response_duration_cr(self) -> Self:
71
- if self.best_overall_response in negative_response_values and self.response_duration:
72
- raise ValueError(
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:
83
+ raise ValueLocError(
73
84
  "If best_overall_response does not indicate a positive response, "
74
- "please leave response_duration blank."
85
+ "please leave response_duration blank.",
86
+ loc="response_duration",
75
87
  )
76
- return self
77
88
 
78
- @model_validator(mode="after")
79
- def validate_days_to_first_response_cr(self) -> Self:
80
- if self.best_overall_response in negative_response_values and self.days_to_first_response:
81
- raise ValueError(
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:
96
+ raise ValueLocError(
82
97
  "If best_overall_response does not indicate a positive response, "
83
- "please leave days_to_first_response blank."
98
+ "please leave days_to_first_response blank.",
99
+ loc="days_to_first_response",
84
100
  )
85
- return self
86
101
 
87
- @model_validator(mode="after")
88
- def validate_days_to_best_response_cr(self) -> Self:
89
- if self.best_overall_response in negative_response_values and self.days_to_best_response:
90
- raise ValueError(
91
- "If best_overall_response does not indicate a positive response, \
92
- please leave days_to_best_response blank."
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:
109
+ raise ValueLocError(
110
+ "If best_overall_response does not indicate a positive response, "
111
+ "please leave days_to_best_response blank.",
112
+ loc="days_to_best_response",
93
113
  )
94
- return self
114
+
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:
122
+ raise ValueLocError(
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",
126
+ )
127
+
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:
135
+ raise ValueLocError(
136
+ "If progression_free_survival_event is not known, " "please leave progression_free_survival blank.",
137
+ loc="progression_free_survival",
138
+ )
139
+
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):
148
+ raise ValueLocError(
149
+ 'Violate "days_to_best_response" >= days_to_first_response"',
150
+ loc="days_to_best_response",
151
+ )
152
+
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):
161
+ raise ValueLocError(
162
+ 'Violate "days_to_first_response" < "days_to_disease_progression"',
163
+ loc="days_to_first_response",
164
+ )
165
+
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):
174
+ raise ValueLocError(
175
+ 'Violate "days_to_best_response" < "days_to_disease_progression"',
176
+ loc="days_to_best_response",
177
+ )
95
178
 
96
179
  @model_validator(mode="after")
97
- def validate_days_to_disease_progression_cr(self) -> Self:
98
- if self.progression in ["No", "Unknown", "Not Applicable"] and self.days_to_disease_progression:
99
- raise ValueError(
100
- "If progression does not indicate confirmed progression of the disease, \
101
- please leave days_to_disease_progress blank."
180
+ def validate_days_to_last_vital_status_chronology(self) -> Self:
181
+ if not self.response:
182
+ return self
183
+
184
+ if not self.response.days_to_last_vital_status:
185
+ return self
186
+
187
+ max_value = max(
188
+ self.response.days_to_last_vital_status or 0,
189
+ self.days_to_first_response or 0,
190
+ self.days_to_best_response or 0,
191
+ self.days_to_disease_progression or 0,
192
+ )
193
+ if (self.response.days_to_last_vital_status or 0) != max_value:
194
+ raise ValueLocError(
195
+ '"days_to_last_vital_status" is not the max of all events. Rule: days_to_last_vital_status '
196
+ ">= max(days_to_first_response,days_to_best_response,days_to_disease_progression)",
197
+ loc="days_to_last_vital_status,days_to_first_response,days_to_best_response,days_to_disease_progression",
102
198
  )
103
199
  return self
104
200
 
105
201
  @model_validator(mode="after")
106
- def validate_progression_free_survival_cr(self) -> Self:
107
- if self.progression_free_survival_event in ["Unknown", "Not Applicable"] and self.progression_free_survival:
108
- raise ValueError(
109
- "If progression_free_survival_event is not known, \
110
- please leave progression_free_survival blank."
202
+ def validate_days_to_death_chronology(self) -> Self:
203
+ if not self.response:
204
+ return self
205
+ if not self.response.days_to_death:
206
+ return self
207
+
208
+ max_value = max(
209
+ self.response.days_to_death or 0,
210
+ self.days_to_first_response or 0,
211
+ self.days_to_best_response or 0,
212
+ self.days_to_disease_progression or 0,
213
+ )
214
+ if (self.response.days_to_death or 0) != max_value:
215
+ raise ValueLocError(
216
+ '"days_to_death" is not the max of all events. Rule: days_to_death'
217
+ ">= max(days_to_first_response,days_to_best_response,days_to_disease_progression)",
218
+ loc="days_to_death,days_to_first_response,days_to_best_response,days_to_disease_progression",
111
219
  )
112
220
  return self
@@ -1,11 +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
+ from cidc_api.models.errors import ValueLocError
5
6
  from cidc_api.models.pydantic.base import Base
6
7
  from cidc_api.models.types import SurgicalProcedure, UberonAnatomicalTerm, YNU
7
8
 
8
9
 
10
+ @forced_validators
9
11
  class Surgery(Base):
10
12
  __data_category__ = "surgery"
11
13
  __cardinality__ = "many"
@@ -42,8 +44,14 @@ class Surgery(Base):
42
44
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=13362284%20and%20ver_nr=1
43
45
  extent_of_residual_disease: str | None = None
44
46
 
45
- @model_validator(mode="after")
46
- def validate_procedure_other_cr(self) -> Self:
47
- if self.procedure == "Other, specify" and not self.procedure_other:
48
- raise ValueError('If procedure is "Other, specify", please provide procedure_other.')
49
- return self
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:
54
+ raise ValueLocError(
55
+ 'If procedure is "Other, specify", please provide procedure_other.',
56
+ loc="procedure_other",
57
+ )
@@ -1,11 +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
 
4
+ from cidc_api.models.errors import ValueLocError
5
5
  from cidc_api.models.pydantic.base import Base
6
6
  from cidc_api.models.types import YNU, TherapyAgentDoseUnits
7
7
 
8
8
 
9
+ @forced_validators
9
10
  class TherapyAgentDose(Base):
10
11
  __data_category__ = "therapy_agent_dose"
11
12
  __cardinality__ = "many"
@@ -54,14 +55,26 @@ class TherapyAgentDose(Base):
54
55
  # Description of the dose changes, misses, or delays.
55
56
  changes_delays_description: str | None = None
56
57
 
57
- @model_validator(mode="after")
58
- def validate_changes_delays_description_cr(self) -> Self:
59
- if self.dose_changes_delays == "Yes" and not self.changes_delays_description:
60
- raise ValueError('If dose_changes_delays is "Yes", please provide changes_delays_description.')
61
- return self
62
-
63
- @model_validator(mode="after")
64
- def validate_planned_dose_units_cr(self) -> Self:
65
- if self.planned_dose and not self.planned_dose_units:
66
- raise ValueError("If planned_dose is provided, please provide planned_dose_units.")
67
- return self
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:
65
+ raise ValueLocError(
66
+ 'If dose_changes_delays is "Yes", please provide changes_delays_description.',
67
+ loc="changes_delays_description",
68
+ )
69
+
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:
77
+ raise ValueLocError(
78
+ "If planned_dose is provided, please provide planned_dose_units.",
79
+ loc="planned_dose_units",
80
+ )