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
boot.py ADDED
@@ -0,0 +1,14 @@
1
+ from os import mkdir, path
2
+ import shutil
3
+
4
+ TEMPLATES_DIR = path.join("/tmp", "templates")
5
+
6
+
7
+ # set up the directories for holding generated templates
8
+ def set_up_templates_directories():
9
+ if path.exists(TEMPLATES_DIR):
10
+ shutil.rmtree(TEMPLATES_DIR)
11
+ mkdir(TEMPLATES_DIR)
12
+ for family in ["assays", "manifests", "analyses"]:
13
+ family_dir = path.join(TEMPLATES_DIR, family)
14
+ mkdir(family_dir)
cidc_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.45"
1
+ __version__ = "1.2.53"
@@ -5,11 +5,11 @@ Any 'UPPER_CASE' variables will be exported as a key-value pair
5
5
  in the `SETTINGS` dictionary defined at the bottom of this file.
6
6
  """
7
7
 
8
- import shutil
9
- from os import environ, path, mkdir
8
+ from os import environ, path
10
9
 
11
10
  from dotenv import load_dotenv
12
11
 
12
+ from boot import TEMPLATES_DIR as templates_dir
13
13
  from .db import get_sqlalchemy_database_uri, cloud_connector
14
14
  from .secrets import get_secrets_manager
15
15
 
@@ -36,14 +36,7 @@ PAGINATION_PAGE_SIZE = 25
36
36
  MAX_PAGINATION_PAGE_SIZE = 200
37
37
  INACTIVE_USER_DAYS = 60
38
38
  MAX_THREADPOOL_WORKERS = 32
39
- TEMPLATES_DIR = path.join("/tmp", "templates")
40
- # Also, set up the directories for holding generated templates
41
- if path.exists(TEMPLATES_DIR):
42
- shutil.rmtree(TEMPLATES_DIR)
43
- mkdir(TEMPLATES_DIR)
44
- for family in ["assays", "manifests", "analyses"]:
45
- family_dir = path.join(TEMPLATES_DIR, family)
46
- mkdir(family_dir)
39
+ TEMPLATES_DIR = templates_dir
47
40
 
48
41
  ### Configure prism encrypt ###
49
42
  if not TESTING:
@@ -85,6 +78,7 @@ GOOGLE_GRANT_DOWNLOAD_PERMISSIONS_TOPIC = environ["GOOGLE_GRANT_DOWNLOAD_PERMISS
85
78
  GOOGLE_HL_CLINICAL_VALIDATION_TOPIC = environ["GOOGLE_HL_CLINICAL_VALIDATION_TOPIC"]
86
79
  GOOGLE_DL_CLINICAL_VALIDATION_TOPIC = environ["GOOGLE_DL_CLINICAL_VALIDATION_TOPIC"]
87
80
  GOOGLE_ASSAY_METADATA_VALIDATION_TOPIC = environ["GOOGLE_ASSAY_METADATA_VALIDATION_TOPIC"]
81
+ GOOGLE_CLINICAL_DATA_INGESTION_PROCESSING_TOPIC = "ingestion"
88
82
  GOOGLE_AND_OPERATOR = " && "
89
83
  GOOGLE_OR_OPERATOR = " || "
90
84
 
@@ -0,0 +1,7 @@
1
+ class ValueLocError(ValueError):
2
+ """A special case of ValueError that allows us to carry along the 'loc' location where a pydantic
3
+ validation error occured."""
4
+
5
+ def __init__(self, *args, loc: str, **kwargs):
6
+ super().__init__(*args, **kwargs)
7
+ self.loc = (loc,) # Make a tuple to match pydantic's loc structure
cidc_api/models/models.py CHANGED
@@ -36,6 +36,7 @@ __all__ = [
36
36
  "ADMIN_FILE_CATEGORIES",
37
37
  "FINAL_JOB_STATUS",
38
38
  "INGESTION_JOB_STATUSES",
39
+ "INGESTION_PHASE_STATUSES",
39
40
  "ASSAY_JOB_COLORS",
40
41
  "CLINICAL_JOB_COLORS",
41
42
  ]
@@ -1048,11 +1049,11 @@ class Permissions(CommonColumns):
1048
1049
  user_email_list.append(user.email)
1049
1050
  grant_lister_access(user.email)
1050
1051
 
1051
- if upload.upload_type in prism.SUPPORTED_SHIPPING_MANIFESTS:
1052
+ if upload.upload_type in prism.SUPPORTED_MANIFESTS:
1052
1053
  # Passed with empty user email list because they will be queried for in CFn
1053
1054
  grant_download_access([], upload.trial_id, "participants info")
1054
1055
  grant_download_access([], upload.trial_id, "samples info")
1055
- elif upload.upload_type not in prism.SUPPORTED_WEIRD_MANIFESTS:
1056
+ else:
1056
1057
  grant_download_access(user_email_list, upload.trial_id, upload.upload_type)
1057
1058
 
1058
1059
  @staticmethod
@@ -3448,6 +3449,8 @@ INGESTION_JOB_STATUSES = [
3448
3449
  "PUBLISHED",
3449
3450
  ]
3450
3451
 
3452
+ INGESTION_PHASE_STATUSES = ["NOT STARTED", "SUCCESS", "FAILED"]
3453
+
3451
3454
  # Business decision to pass hex codes from the backend though that should be done by the front end...
3452
3455
  CLINICAL_JOB_COLORS = {
3453
3456
  "DRAFT": "",
@@ -3492,6 +3495,13 @@ class IngestionJobs(CommonColumns):
3492
3495
  intake_path = Column(String, nullable=True)
3493
3496
  uploader_email = Column(String, nullable=True)
3494
3497
  is_template_downloaded = Column(Boolean, nullable=True)
3498
+ master_aa_version = Column(Integer, nullable=True)
3499
+ ingestion_phase_status = Column(
3500
+ "ingestion_phase_status",
3501
+ Enum(*INGESTION_PHASE_STATUSES, name="ingestion_phase_status"),
3502
+ nullable=False,
3503
+ default=INGESTION_PHASE_STATUSES[0],
3504
+ )
3495
3505
 
3496
3506
  @staticmethod
3497
3507
  @with_default_session
@@ -3502,6 +3512,7 @@ class IngestionJobs(CommonColumns):
3502
3512
  error_status: str = None,
3503
3513
  pending: Boolean = False,
3504
3514
  job_type: str = "clinical",
3515
+ ingestion_phase_status=INGESTION_PHASE_STATUSES[0],
3505
3516
  assay_type: str = None,
3506
3517
  batch_id: str = None,
3507
3518
  submission_id: str = None,
@@ -3523,6 +3534,7 @@ class IngestionJobs(CommonColumns):
3523
3534
  intake_path=intake_path,
3524
3535
  start_date=start_date,
3525
3536
  uploader_email=uploader_email,
3537
+ ingestion_phase_status=ingestion_phase_status,
3526
3538
  )
3527
3539
  new_job.insert(session=session)
3528
3540
  return new_job
@@ -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,9 @@
1
- from typing import Self
2
-
3
- from pydantic import 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
6
+ from cidc_api.models.errors import ValueLocError
7
7
  from cidc_api.models.types import (
8
8
  CTCAEEventTerm,
9
9
  CTCAEEventCode,
@@ -17,6 +17,7 @@ from cidc_api.models.types import (
17
17
  )
18
18
 
19
19
 
20
+ @forced_validators
20
21
  class AdverseEvent(Base):
21
22
  __data_category__ = "adverse_event"
22
23
  __cardinality__ = "many"
@@ -75,26 +76,52 @@ class AdverseEvent(Base):
75
76
  # The individual therapy (therapy agent, radiotherapy, surgery, stem cell transplant) in the treatment that is attributed to the adverse event.
76
77
  individual_therapy: str | None = None
77
78
 
78
- @model_validator(mode="after")
79
- def validate_term_and_code_cr(self) -> Self:
80
- if not self.event_term and not self.event_code:
81
- raise ValueError("Please provide event_term or event_code or both")
82
- return self
83
-
84
- @model_validator(mode="after")
85
- def validate_event_other_specify_cr(self) -> Self:
86
- if (
87
- self.severity_grade_system == "CTCAE"
88
- and is_ctcae_other_term(self.event_term)
89
- and not self.event_other_specify
90
- ):
91
- raise ValueError(
92
- 'If severity_grade_system is "CTCAE" and the event_code or event_term are of type "Other, specify", please provide event_other_specify'
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:
86
+ raise ValueLocError(
87
+ "Please provide event_term or event_code or both",
88
+ loc="event_term,event_code",
89
+ )
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:
99
+ raise ValueLocError(
100
+ 'If severity_grade_system is "CTCAE" and the event_code or event_term are of type '
101
+ '"Other, specify", please provide event_other_specify',
102
+ loc="event_other_specify",
103
+ )
104
+
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:
112
+ raise ValueLocError(
113
+ "If event_other_specify is provided, please provide system_organ_class.", loc="system_organ_class"
93
114
  )
94
- return self
95
115
 
96
- @model_validator(mode="after")
97
- def validate_system_organ_class_cr(self) -> Self:
98
- if self.event_other_specify and not self.system_organ_class:
99
- raise ValueError("If event_other_specify is provided, please provide system_organ_class.")
100
- return self
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):
124
+ raise ValueLocError(
125
+ 'Violate "days_to_onset_of_event" <= "days_to_resolution_of_event"',
126
+ loc="days_to_resolution_of_event",
127
+ )
@@ -1,11 +1,13 @@
1
- from typing import Self
1
+ from typing import Any
2
2
 
3
- from pydantic import model_validator
3
+ from cidc_api.models.pydantic.base import forced_validator, forced_validators
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 ICD10CMCode, ICD10CMTerm
7
8
 
8
9
 
10
+ @forced_validators
9
11
  class Comorbidity(Base):
10
12
  __data_category__ = "comorbidity"
11
13
  __cardinality__ = "many"
@@ -27,10 +29,15 @@ class Comorbidity(Base):
27
29
  # A descriptive string that names or briefly describes the comorbidity.
28
30
  comorbidity_other: str | None = None
29
31
 
30
- @model_validator(mode="after")
31
- def validate_code_or_term_or_other_cr(self) -> Self:
32
- if not self.comorbidity_code and not self.comorbidity_term and not self.comorbidity_other:
33
- raise ValueError(
34
- 'Please provide at least one of "comorbidity_code", "comorbidity_term" or "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:
40
+ raise ValueLocError(
41
+ 'Please provide at least one of "comorbidity_code", "comorbidity_term" or "comorbidity_other".',
42
+ loc="comorbidity_code,comorbidity_term,comorbidity_other",
35
43
  )
36
- return self
@@ -1,7 +1,9 @@
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
 
6
+ from cidc_api.models.errors import ValueLocError
5
7
  from cidc_api.models.pydantic.base import Base
6
8
  from cidc_api.models.types import (
7
9
  Sex,
@@ -16,6 +18,7 @@ from cidc_api.models.types import (
16
18
  )
17
19
 
18
20
 
21
+ @forced_validators
19
22
  class Demographic(Base):
20
23
  __data_category__ = "demographic"
21
24
  __cardinality__ = "one"
@@ -90,34 +93,48 @@ class Demographic(Base):
90
93
  # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2681552%20and%20ver_nr=1
91
94
  highest_level_of_education: Education | None = None
92
95
 
93
- @model_validator(mode="after")
94
- def validate_age_at_enrollment_cr(self) -> Self:
95
- if self.age_90_or_over:
96
- if self.age_at_enrollment or self.age_at_enrollment_units:
97
- raise ValueError(
98
- 'If "age_90_or_over" is "Yes" then "age_at_enrollment" and "age_at_enrollment_units" must be blank.'
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 == True or age_90_or_over == "Yes":
104
+ if age_at_enrollment or age_at_enrollment_units:
105
+ raise ValueLocError(
106
+ 'If "age_90_or_over" is "Yes" then "age_at_enrollment" and "age_at_enrollment_units" must be blank.',
107
+ loc="age_at_enrollment",
99
108
  )
100
- else:
101
- if not self.age_at_enrollment or not self.age_at_enrollment_units:
102
- raise ValueError(
103
- 'If "age_90_or_over" is "No" then "age_at_enrollment" and "age_at_enrollment_units" are required.'
109
+ elif age_90_or_over == False or age_90_or_over == "No":
110
+ if not age_at_enrollment or not age_at_enrollment_units:
111
+ raise ValueLocError(
112
+ 'If "age_90_or_over" is "No" then "age_at_enrollment" and "age_at_enrollment_units" are required.',
113
+ loc="age_at_enrollment",
104
114
  )
105
- return self
106
115
 
107
- @model_validator(mode="after")
108
- def validate_age_at_enrollment_value(self) -> Self:
109
- if not self.age_90_or_over:
110
- age_in_years = (
111
- self.age_at_enrollment if self.age_at_enrollment_units == "Years" else self.age_at_enrollment / 365.25
112
- )
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 == True or age_90_or_over == "Yes":
124
+ age_in_years = age_at_enrollment if age_at_enrollment_units == "Years" else age_at_enrollment / 365.25
113
125
  if age_in_years >= 90:
114
- raise ValueError('"age_at_enrollment" cannot represent a value greater than 90 years of age.')
115
- return self
116
-
117
- @model_validator(mode="after")
118
- def validate_body_surface_area_units_cr(self) -> Self:
119
- if self.body_surface_area and not self.body_surface_area_units:
120
- raise ValueError(
121
- 'If "body_surface_area" is provided then "body_surface_area_units_other" must also be provided.'
126
+ raise ValueLocError(
127
+ '"age_at_enrollment" cannot represent a value greater than 90 years of age.',
128
+ loc="age_at_enrollment",
129
+ )
130
+
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:
138
+ raise ValueLocError(
139
+ 'If "body_surface_area" is provided then "body_surface_area_units" must also be provided.'
122
140
  )
123
- return self
@@ -1,6 +1,9 @@
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
3
 
4
+ from typing import List, Self, Annotated, get_args, Any
5
+
6
+ from cidc_api.models.errors import ValueLocError
4
7
  from cidc_api.models.pydantic.base import Base
5
8
  from cidc_api.models.types import (
6
9
  TumorGrade,
@@ -20,6 +23,7 @@ from cidc_api.models.types import (
20
23
  )
21
24
 
22
25
 
26
+ @forced_validators
23
27
  class Disease(Base):
24
28
  __data_category__ = "disease"
25
29
  __cardinality__ = "many"
@@ -92,67 +96,105 @@ class Disease(Base):
92
96
 
93
97
  extramedullary_organ: Annotated[List[UberonAnatomicalTerm] | None, BeforeValidator(Base.split_list)] = []
94
98
 
95
- @model_validator(mode="after")
96
- def validate_code_or_term_or_description_cr(self) -> Self:
97
- if not self.morphological_code and not self.morphological_term and not self.cancer_type_description:
98
- raise ValueError(
99
- 'Please provide at least one of "morphological_code", "morphological_term" or "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:
107
+ raise ValueLocError(
108
+ 'Please provide at least one of "morphological_code", "morphological_term" or "cancer_type_description".',
109
+ loc="morphological_code",
100
110
  )
101
- return self
102
111
 
103
- @model_validator(mode="after")
104
- def validate_cancer_stage_system_version(self) -> Self:
105
- msg = f"{self.cancer_stage_system_version} is not applicable to {self.cancer_stage_system}"
106
- if self.cancer_stage_system == "AJCC" and self.cancer_stage_system_version not in get_args(
107
- CancerStageSystemVersionAJCC
108
- ):
109
- raise ValueError(msg)
110
- elif self.cancer_stage_system == "RISS" and self.cancer_stage_system_version not in get_args(
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):
120
+ raise ValueLocError(msg, loc="cancer_stage_system")
121
+ elif cancer_stage_system == "RISS" and cancer_stage_system_version not in get_args(
111
122
  CancerStageSystemVersionRISS
112
123
  ):
113
- raise ValueError(msg)
114
- elif self.cancer_stage_system == "FIGO" and self.cancer_stage_system_version not in get_args(
124
+ raise ValueLocError(msg, loc="cancer_stage_system")
125
+ elif cancer_stage_system == "FIGO" and cancer_stage_system_version not in get_args(
115
126
  CancerStageSystemVersionFIGO
116
127
  ):
117
- raise ValueError(msg)
118
- return self
119
-
120
- @model_validator(mode="after")
121
- def validate_cancer_stage_system_version_cr(self) -> Self:
122
- if self.cancer_stage_system != "Not Applicable" and not self.cancer_stage_system_version:
123
- raise ValueError(
124
- f'Please provide cancer_stage_system_version when cancer_stage_system is "{self.cancer_stage_system}"'
128
+ raise ValueLocError(msg, loc="cancer_stage_system")
129
+
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:
137
+ raise ValueLocError(
138
+ f'Please provide cancer_stage_system_version when cancer_stage_system is "{cancer_stage_system}"',
139
+ loc="cancer_stage_system_version",
140
+ )
141
+
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:
149
+ raise ValueLocError(
150
+ f'Please provide cancer_stage when cancer_stage_system is "{cancer_stage_system}"',
151
+ loc="cancer_stage",
152
+ )
153
+
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:
161
+ raise ValueLocError(
162
+ f'Please provide t_category when cancer_stage_system is "{cancer_stage_system}"',
163
+ loc="t_category",
125
164
  )
126
- return self
127
-
128
- @model_validator(mode="after")
129
- def validate_cancer_stage_cr(self) -> Self:
130
- if self.cancer_stage_system != "Not Applicable" and not self.cancer_stage:
131
- raise ValueError(f'Please provide cancer_stage when cancer_stage_system is "{self.cancer_stage_system}"')
132
- return self
133
-
134
- @model_validator(mode="after")
135
- def validate_t_category_cr(self) -> Self:
136
- if self.cancer_stage_system == "AJCC" and not self.t_category:
137
- raise ValueError(f'Please provide t_category when cancer_stage_system is "{self.cancer_stage_system}"')
138
- return self
139
-
140
- @model_validator(mode="after")
141
- def validate_n_category_cr(self) -> Self:
142
- if self.cancer_stage_system == "AJCC" and not self.n_category:
143
- raise ValueError(f'Please provide n_category when cancer_stage_system is "{self.cancer_stage_system}"')
144
- return self
145
-
146
- @model_validator(mode="after")
147
- def validate_m_category_cr(self) -> Self:
148
- if self.cancer_stage_system == "AJCC" and not self.m_category:
149
- raise ValueError(f'Please provide m_category when cancer_stage_system is "{self.cancer_stage_system}"')
150
- return self
151
-
152
- @model_validator(mode="after")
153
- def validate_extramedullary_organ_cr(self) -> Self:
154
- if self.solely_extramedullary_disease in ["No", "Unknown"] and self.extramedullary_organ:
155
- raise ValueError(
156
- "If solely_extramedullary_disease indicates no disease, please leave extramedullary_organ blank."
165
+
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:
173
+ raise ValueLocError(
174
+ f'Please provide n_category when cancer_stage_system is "{cancer_stage_system}"',
175
+ loc="n_category",
176
+ )
177
+
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:
185
+ raise ValueLocError(
186
+ f'Please provide m_category when cancer_stage_system is "{cancer_stage_system}"',
187
+ loc="m_category",
188
+ )
189
+
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:
197
+ raise ValueLocError(
198
+ "If solely_extramedullary_disease indicates no disease, please leave extramedullary_organ blank.",
199
+ loc="extramedullary_organ",
157
200
  )
158
- return self