nci-cidc-api-modules 1.2.34__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.
- boot.py +14 -0
- cidc_api/__init__.py +1 -0
- cidc_api/config/db.py +21 -1
- cidc_api/config/settings.py +5 -10
- cidc_api/models/__init__.py +0 -2
- cidc_api/models/data.py +15 -6
- cidc_api/models/db/stage1/__init__.py +56 -0
- cidc_api/models/db/stage1/additional_treatment_orm.py +22 -0
- cidc_api/models/db/stage1/adverse_event_orm.py +46 -0
- cidc_api/models/db/stage1/base_orm.py +7 -0
- cidc_api/models/db/stage1/baseline_clinical_assessment_orm.py +22 -0
- cidc_api/models/db/stage1/comorbidity_orm.py +23 -0
- cidc_api/models/db/stage1/consent_group_orm.py +32 -0
- cidc_api/models/db/stage1/demographic_orm.py +47 -0
- cidc_api/models/db/stage1/disease_orm.py +52 -0
- cidc_api/models/db/stage1/exposure_orm.py +22 -0
- cidc_api/models/db/stage1/gvhd_diagnosis_acute_orm.py +34 -0
- cidc_api/models/db/stage1/gvhd_diagnosis_chronic_orm.py +36 -0
- cidc_api/models/db/stage1/gvhd_organ_acute_orm.py +21 -0
- cidc_api/models/db/stage1/gvhd_organ_chronic_orm.py +21 -0
- cidc_api/models/db/stage1/medical_history_orm.py +30 -0
- cidc_api/models/db/stage1/other_malignancy_orm.py +29 -0
- cidc_api/models/db/stage1/participant_orm.py +77 -0
- cidc_api/models/db/stage1/prior_treatment_orm.py +29 -0
- cidc_api/models/db/stage1/radiotherapy_dose_orm.py +39 -0
- cidc_api/models/db/stage1/response_by_system_orm.py +30 -0
- cidc_api/models/db/stage1/response_orm.py +28 -0
- cidc_api/models/db/stage1/specimen_orm.py +46 -0
- cidc_api/models/db/stage1/stem_cell_transplant_orm.py +25 -0
- cidc_api/models/db/stage1/surgery_orm.py +27 -0
- cidc_api/models/db/stage1/therapy_agent_dose_orm.py +31 -0
- cidc_api/models/db/stage1/treatment_orm.py +38 -0
- cidc_api/models/db/stage1/trial_orm.py +35 -0
- cidc_api/models/db/stage2/additional_treatment_orm.py +6 -7
- cidc_api/models/db/stage2/administrative_person_orm.py +4 -4
- cidc_api/models/db/stage2/administrative_role_assignment_orm.py +4 -4
- cidc_api/models/db/stage2/adverse_event_orm.py +11 -13
- cidc_api/models/db/stage2/arm_orm.py +3 -3
- cidc_api/models/db/stage2/base_orm.py +7 -0
- cidc_api/models/db/stage2/baseline_clinical_assessment_orm.py +5 -7
- cidc_api/models/db/stage2/cohort_orm.py +3 -3
- cidc_api/models/db/stage2/comorbidity_orm.py +6 -8
- cidc_api/models/db/stage2/consent_group_orm.py +4 -4
- cidc_api/models/db/stage2/contact_orm.py +16 -20
- cidc_api/models/db/stage2/demographic_orm.py +3 -3
- cidc_api/models/db/stage2/disease_orm.py +4 -4
- cidc_api/models/db/stage2/exposure_orm.py +3 -3
- cidc_api/models/db/stage2/file_orm.py +6 -9
- cidc_api/models/db/stage2/gvhd_diagnosis_acute_orm.py +4 -4
- cidc_api/models/db/stage2/gvhd_diagnosis_chronic_orm.py +4 -6
- cidc_api/models/db/stage2/gvhd_organ_acute_orm.py +3 -3
- cidc_api/models/db/stage2/gvhd_organ_chronic_orm.py +3 -3
- cidc_api/models/db/stage2/institution_orm.py +7 -7
- cidc_api/models/db/stage2/medical_history_orm.py +9 -9
- cidc_api/models/db/stage2/other_clinical_endpoint_orm.py +8 -12
- cidc_api/models/db/stage2/other_malignancy_orm.py +8 -10
- cidc_api/models/db/stage2/participant_orm.py +23 -24
- cidc_api/models/db/stage2/prior_treatment_orm.py +12 -13
- cidc_api/models/db/stage2/publication_orm.py +9 -11
- cidc_api/models/db/stage2/radiotherapy_dose_orm.py +8 -9
- cidc_api/models/db/stage2/response_by_system_orm.py +3 -3
- cidc_api/models/db/stage2/response_orm.py +3 -3
- cidc_api/models/db/stage2/shipment_orm.py +17 -17
- cidc_api/models/db/stage2/shipment_specimen_orm.py +4 -4
- cidc_api/models/db/stage2/specimen_orm.py +7 -6
- cidc_api/models/db/stage2/stem_cell_transplant_orm.py +6 -7
- cidc_api/models/db/stage2/surgery_orm.py +6 -7
- cidc_api/models/db/stage2/therapy_agent_dose_orm.py +7 -8
- cidc_api/models/db/stage2/treatment_orm.py +15 -15
- cidc_api/models/db/stage2/trial_orm.py +15 -17
- cidc_api/models/errors.py +7 -0
- cidc_api/models/files/facets.py +4 -0
- cidc_api/models/models.py +167 -11
- cidc_api/models/pydantic/base.py +109 -0
- cidc_api/models/pydantic/stage1/__init__.py +56 -0
- cidc_api/models/pydantic/stage1/additional_treatment.py +23 -0
- cidc_api/models/pydantic/stage1/adverse_event.py +127 -0
- cidc_api/models/pydantic/stage1/baseline_clinical_assessment.py +23 -0
- cidc_api/models/pydantic/stage1/comorbidity.py +43 -0
- cidc_api/models/pydantic/stage1/consent_group.py +30 -0
- cidc_api/models/pydantic/stage1/demographic.py +140 -0
- cidc_api/models/pydantic/stage1/disease.py +200 -0
- cidc_api/models/pydantic/stage1/exposure.py +38 -0
- cidc_api/models/pydantic/stage1/gvhd_diagnosis_acute.py +33 -0
- cidc_api/models/pydantic/stage1/gvhd_diagnosis_chronic.py +32 -0
- cidc_api/models/pydantic/stage1/gvhd_organ_acute.py +22 -0
- cidc_api/models/pydantic/stage1/gvhd_organ_chronic.py +23 -0
- cidc_api/models/pydantic/stage1/medical_history.py +43 -0
- cidc_api/models/pydantic/stage1/other_malignancy.py +55 -0
- cidc_api/models/pydantic/stage1/participant.py +63 -0
- cidc_api/models/pydantic/stage1/prior_treatment.py +45 -0
- cidc_api/models/pydantic/stage1/radiotherapy_dose.py +92 -0
- cidc_api/models/pydantic/stage1/response.py +84 -0
- cidc_api/models/pydantic/stage1/response_by_system.py +220 -0
- cidc_api/models/pydantic/stage1/specimen.py +31 -0
- cidc_api/models/pydantic/stage1/stem_cell_transplant.py +35 -0
- cidc_api/models/pydantic/stage1/surgery.py +57 -0
- cidc_api/models/pydantic/stage1/therapy_agent_dose.py +80 -0
- cidc_api/models/pydantic/stage1/treatment.py +64 -0
- cidc_api/models/pydantic/stage1/trial.py +45 -0
- cidc_api/models/pydantic/stage2/additional_treatment.py +2 -4
- cidc_api/models/pydantic/stage2/administrative_person.py +1 -1
- cidc_api/models/pydantic/stage2/administrative_role_assignment.py +2 -2
- cidc_api/models/pydantic/stage2/adverse_event.py +1 -1
- cidc_api/models/pydantic/stage2/arm.py +2 -2
- cidc_api/models/pydantic/stage2/baseline_clinical_assessment.py +1 -1
- cidc_api/models/pydantic/stage2/cohort.py +1 -1
- cidc_api/models/pydantic/stage2/comorbidity.py +1 -1
- cidc_api/models/pydantic/stage2/consent_group.py +2 -2
- cidc_api/models/pydantic/stage2/contact.py +1 -1
- cidc_api/models/pydantic/stage2/demographic.py +1 -1
- cidc_api/models/pydantic/stage2/disease.py +1 -1
- cidc_api/models/pydantic/stage2/exposure.py +1 -1
- cidc_api/models/pydantic/stage2/file.py +2 -2
- cidc_api/models/pydantic/stage2/gvhd_diagnosis_acute.py +1 -1
- cidc_api/models/pydantic/stage2/gvhd_diagnosis_chronic.py +1 -1
- cidc_api/models/pydantic/stage2/gvhd_organ_acute.py +1 -1
- cidc_api/models/pydantic/stage2/gvhd_organ_chronic.py +1 -1
- cidc_api/models/pydantic/stage2/institution.py +1 -1
- cidc_api/models/pydantic/stage2/medical_history.py +1 -1
- cidc_api/models/pydantic/stage2/other_clinical_endpoint.py +1 -1
- cidc_api/models/pydantic/stage2/other_malignancy.py +1 -1
- cidc_api/models/pydantic/stage2/participant.py +6 -3
- cidc_api/models/pydantic/stage2/prior_treatment.py +6 -15
- cidc_api/models/pydantic/stage2/publication.py +2 -2
- cidc_api/models/pydantic/stage2/radiotherapy_dose.py +1 -1
- cidc_api/models/pydantic/stage2/response.py +2 -2
- cidc_api/models/pydantic/stage2/response_by_system.py +1 -1
- cidc_api/models/pydantic/stage2/shipment.py +2 -2
- cidc_api/models/pydantic/stage2/shipment_specimen.py +1 -1
- cidc_api/models/pydantic/stage2/specimen.py +6 -3
- cidc_api/models/pydantic/stage2/stem_cell_transplant.py +2 -2
- cidc_api/models/pydantic/stage2/surgery.py +1 -1
- cidc_api/models/pydantic/stage2/therapy_agent_dose.py +1 -1
- cidc_api/models/pydantic/stage2/treatment.py +1 -1
- cidc_api/models/pydantic/stage2/trial.py +8 -10
- cidc_api/models/types.py +30 -16
- cidc_api/shared/assay_handling.py +68 -0
- cidc_api/shared/auth.py +5 -5
- cidc_api/shared/file_handling.py +18 -4
- cidc_api/shared/gcloud_client.py +96 -16
- cidc_api/shared/utils.py +18 -9
- cidc_api/telemetry.py +101 -0
- {nci_cidc_api_modules-1.2.34.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/METADATA +25 -15
- nci_cidc_api_modules-1.2.53.dist-info/RECORD +167 -0
- {nci_cidc_api_modules-1.2.34.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/WHEEL +1 -1
- {nci_cidc_api_modules-1.2.34.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/top_level.txt +1 -0
- cidc_api/models/db/base_orm.py +0 -25
- cidc_api/models/pydantic/stage2/base.py +0 -48
- nci_cidc_api_modules-1.2.34.dist-info/RECORD +0 -109
- {nci_cidc_api_modules-1.2.34.dist-info → nci_cidc_api_modules-1.2.53.dist-info}/licenses/LICENSE +0 -0
cidc_api/models/models.py
CHANGED
|
@@ -28,6 +28,7 @@ __all__ = [
|
|
|
28
28
|
"JobFileCategories",
|
|
29
29
|
"CategoryDataElements",
|
|
30
30
|
"ValidationConfigs",
|
|
31
|
+
"MASTER_APPENDIX_A",
|
|
31
32
|
"TRIAL_APPENDIX_A",
|
|
32
33
|
"TRIAL_APPENDIX_A_CELL_THAT_ENDS_THE_HEADER",
|
|
33
34
|
"REQUEST_LETTER",
|
|
@@ -35,7 +36,9 @@ __all__ = [
|
|
|
35
36
|
"ADMIN_FILE_CATEGORIES",
|
|
36
37
|
"FINAL_JOB_STATUS",
|
|
37
38
|
"INGESTION_JOB_STATUSES",
|
|
38
|
-
"
|
|
39
|
+
"INGESTION_PHASE_STATUSES",
|
|
40
|
+
"ASSAY_JOB_COLORS",
|
|
41
|
+
"CLINICAL_JOB_COLORS",
|
|
39
42
|
]
|
|
40
43
|
|
|
41
44
|
import hashlib
|
|
@@ -1046,11 +1049,11 @@ class Permissions(CommonColumns):
|
|
|
1046
1049
|
user_email_list.append(user.email)
|
|
1047
1050
|
grant_lister_access(user.email)
|
|
1048
1051
|
|
|
1049
|
-
if upload.upload_type in prism.
|
|
1052
|
+
if upload.upload_type in prism.SUPPORTED_MANIFESTS:
|
|
1050
1053
|
# Passed with empty user email list because they will be queried for in CFn
|
|
1051
1054
|
grant_download_access([], upload.trial_id, "participants info")
|
|
1052
1055
|
grant_download_access([], upload.trial_id, "samples info")
|
|
1053
|
-
|
|
1056
|
+
else:
|
|
1054
1057
|
grant_download_access(user_email_list, upload.trial_id, upload.upload_type)
|
|
1055
1058
|
|
|
1056
1059
|
@staticmethod
|
|
@@ -3261,6 +3264,7 @@ def upload_manifest_json(
|
|
|
3261
3264
|
return manifest_upload.id
|
|
3262
3265
|
|
|
3263
3266
|
|
|
3267
|
+
MASTER_APPENDIX_A = "master_appendix_a"
|
|
3264
3268
|
TRIAL_APPENDIX_A = "trial_appendix_a"
|
|
3265
3269
|
REQUEST_LETTER = "request_letter"
|
|
3266
3270
|
DETAILED_VALIDATION = "detailed_validation"
|
|
@@ -3360,6 +3364,16 @@ class PreprocessedFiles(CommonColumns):
|
|
|
3360
3364
|
query = cls.add_job_filter(query, job_id)
|
|
3361
3365
|
return query.all()
|
|
3362
3366
|
|
|
3367
|
+
@classmethod
|
|
3368
|
+
@with_default_session
|
|
3369
|
+
def get_latest_current_file(
|
|
3370
|
+
cls, file_category: str, job_id: int = None, session: Session = None
|
|
3371
|
+
) -> Optional["PreprocessedFiles"]:
|
|
3372
|
+
"""Return the latest 'current' file for the given category and job_id. Returns None if no file exists."""
|
|
3373
|
+
query = session.query(cls).filter_by(file_category=file_category, status="current")
|
|
3374
|
+
query = cls.add_job_filter(query, job_id)
|
|
3375
|
+
return query.order_by(cls.version.desc()).first()
|
|
3376
|
+
|
|
3363
3377
|
@classmethod
|
|
3364
3378
|
@with_default_session
|
|
3365
3379
|
def get_file_by_category_and_version(
|
|
@@ -3435,8 +3449,10 @@ INGESTION_JOB_STATUSES = [
|
|
|
3435
3449
|
"PUBLISHED",
|
|
3436
3450
|
]
|
|
3437
3451
|
|
|
3452
|
+
INGESTION_PHASE_STATUSES = ["NOT STARTED", "SUCCESS", "FAILED"]
|
|
3453
|
+
|
|
3438
3454
|
# Business decision to pass hex codes from the backend though that should be done by the front end...
|
|
3439
|
-
|
|
3455
|
+
CLINICAL_JOB_COLORS = {
|
|
3440
3456
|
"DRAFT": "",
|
|
3441
3457
|
"INITIAL SUBMISSION": "#ACCAD7",
|
|
3442
3458
|
"VALIDATION REVIEW": "#DABE90",
|
|
@@ -3444,6 +3460,13 @@ INGESTION_JOB_COLORS = {
|
|
|
3444
3460
|
"INGESTION": "#8FCEC7",
|
|
3445
3461
|
"PUBLISHED": "#90D9E6",
|
|
3446
3462
|
}
|
|
3463
|
+
ASSAY_JOB_COLORS = {
|
|
3464
|
+
"INITIAL SUBMISSION": "#43807E",
|
|
3465
|
+
"VALIDATION REVIEW": "#906F3F",
|
|
3466
|
+
"REVISION SUBMISSION": "#95358A",
|
|
3467
|
+
"INGESTION": "#542C88",
|
|
3468
|
+
"PUBLISHED": "#1C81A0",
|
|
3469
|
+
}
|
|
3447
3470
|
# TODO If have "CANCELLED" concept or other final status, add here
|
|
3448
3471
|
FINAL_JOB_STATUS = ["PUBLISHED"]
|
|
3449
3472
|
TRIAL_APPENDIX_A_CELL_THAT_ENDS_THE_HEADER = "Data Category"
|
|
@@ -3465,11 +3488,54 @@ class IngestionJobs(CommonColumns):
|
|
|
3465
3488
|
pending = Column(Boolean, nullable=False, default=False)
|
|
3466
3489
|
start_date = Column(DateTime, nullable=True)
|
|
3467
3490
|
error_status = Column(String, nullable=True)
|
|
3491
|
+
job_type = Column(String, nullable=False, default="clinical")
|
|
3492
|
+
assay_type = Column(String, nullable=True)
|
|
3493
|
+
batch_id = Column(String, nullable=True)
|
|
3494
|
+
submission_id = Column(String, nullable=True)
|
|
3495
|
+
intake_path = Column(String, nullable=True)
|
|
3496
|
+
uploader_email = Column(String, nullable=True)
|
|
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
|
+
)
|
|
3468
3505
|
|
|
3469
3506
|
@staticmethod
|
|
3470
3507
|
@with_default_session
|
|
3471
|
-
def create(
|
|
3472
|
-
|
|
3508
|
+
def create(
|
|
3509
|
+
trial_id: str,
|
|
3510
|
+
status: str,
|
|
3511
|
+
version: int,
|
|
3512
|
+
error_status: str = None,
|
|
3513
|
+
pending: Boolean = False,
|
|
3514
|
+
job_type: str = "clinical",
|
|
3515
|
+
ingestion_phase_status=INGESTION_PHASE_STATUSES[0],
|
|
3516
|
+
assay_type: str = None,
|
|
3517
|
+
batch_id: str = None,
|
|
3518
|
+
submission_id: str = None,
|
|
3519
|
+
intake_path: str = None,
|
|
3520
|
+
start_date: datetime = None,
|
|
3521
|
+
uploader_email: str = None,
|
|
3522
|
+
session: Session = None,
|
|
3523
|
+
):
|
|
3524
|
+
new_job = IngestionJobs(
|
|
3525
|
+
trial_id=trial_id,
|
|
3526
|
+
status=status,
|
|
3527
|
+
error_status=error_status,
|
|
3528
|
+
version=version,
|
|
3529
|
+
pending=pending,
|
|
3530
|
+
job_type=job_type,
|
|
3531
|
+
assay_type=assay_type,
|
|
3532
|
+
batch_id=batch_id,
|
|
3533
|
+
submission_id=submission_id,
|
|
3534
|
+
intake_path=intake_path,
|
|
3535
|
+
start_date=start_date,
|
|
3536
|
+
uploader_email=uploader_email,
|
|
3537
|
+
ingestion_phase_status=ingestion_phase_status,
|
|
3538
|
+
)
|
|
3473
3539
|
new_job.insert(session=session)
|
|
3474
3540
|
return new_job
|
|
3475
3541
|
|
|
@@ -3494,29 +3560,43 @@ class IngestionJobs(CommonColumns):
|
|
|
3494
3560
|
|
|
3495
3561
|
@classmethod
|
|
3496
3562
|
@with_default_session
|
|
3497
|
-
def get_jobs_by_trial(
|
|
3498
|
-
|
|
3563
|
+
def get_jobs_by_trial(
|
|
3564
|
+
cls, trial_id: str, job_type: str = "clinical", session: Session = None
|
|
3565
|
+
) -> list["IngestionJobs"]:
|
|
3566
|
+
return (
|
|
3567
|
+
session.query(cls)
|
|
3568
|
+
.filter(cls.trial_id == trial_id, cls.job_type == job_type)
|
|
3569
|
+
.order_by(cls.version.desc())
|
|
3570
|
+
.all()
|
|
3571
|
+
)
|
|
3499
3572
|
|
|
3500
3573
|
@classmethod
|
|
3501
3574
|
@with_default_session
|
|
3502
|
-
def get_open_job_by_trial(
|
|
3575
|
+
def get_open_job_by_trial(
|
|
3576
|
+
cls, trial_id: str, job_type: str = "clinical", session: Session = None
|
|
3577
|
+
) -> Optional["IngestionJobs"]:
|
|
3503
3578
|
"""Return the open job for a given trial if it exists."""
|
|
3504
3579
|
return (
|
|
3505
3580
|
session.query(cls)
|
|
3506
3581
|
.filter(
|
|
3507
3582
|
cls.trial_id == trial_id,
|
|
3583
|
+
cls.job_type == job_type,
|
|
3508
3584
|
cls.status.notin_(FINAL_JOB_STATUS),
|
|
3509
3585
|
)
|
|
3510
3586
|
.order_by(cls._created.desc())
|
|
3511
3587
|
.first()
|
|
3512
3588
|
)
|
|
3513
3589
|
|
|
3590
|
+
@classmethod
|
|
3591
|
+
def get_jobs_for_user(cls, user: Users, job_type: str = None) -> list["IngestionJobs"]:
|
|
3592
|
+
return cls.get_assay_jobs_for_user(user) if job_type == "assay" else cls.get_clinical_jobs_for_user(user)
|
|
3593
|
+
|
|
3514
3594
|
@classmethod
|
|
3515
3595
|
@with_default_session
|
|
3516
|
-
def
|
|
3596
|
+
def get_clinical_jobs_for_user(cls, user: Users, session: Session = None) -> list["IngestionJobs"]:
|
|
3517
3597
|
if user.role not in [CIDCRole.ADMIN.value, CIDCRole.CLINICAL_TRIAL_USER.value]:
|
|
3518
3598
|
return []
|
|
3519
|
-
job_query = session.query(cls).filter(cls.status.notin_(["DRAFT"]))
|
|
3599
|
+
job_query = session.query(cls).filter(cls.status.notin_(["DRAFT"]), cls.job_type == "clinical")
|
|
3520
3600
|
if (
|
|
3521
3601
|
user.role != CIDCRole.ADMIN.value
|
|
3522
3602
|
and not session.query(Permissions)
|
|
@@ -3539,6 +3619,81 @@ class IngestionJobs(CommonColumns):
|
|
|
3539
3619
|
job_query = job_query.filter(cls.trial_id.in_(map(lambda x: x.trial_id, authorized_trials)))
|
|
3540
3620
|
return job_query.order_by(cls._created.desc()).all()
|
|
3541
3621
|
|
|
3622
|
+
@classmethod
|
|
3623
|
+
@with_default_session
|
|
3624
|
+
def get_assay_jobs_for_user(cls, user: Users, session: Session = None) -> list["IngestionJobs"]:
|
|
3625
|
+
# TODO allow more than just Admin role and get authorized trials based on permissions
|
|
3626
|
+
if user.role not in [CIDCRole.ADMIN.value]:
|
|
3627
|
+
return []
|
|
3628
|
+
return session.query(cls).filter(cls.job_type == "assay").order_by(cls._created.desc()).all()
|
|
3629
|
+
|
|
3630
|
+
@classmethod
|
|
3631
|
+
@with_default_session
|
|
3632
|
+
def get_unique_assay_job(
|
|
3633
|
+
cls,
|
|
3634
|
+
trial_id: str,
|
|
3635
|
+
assay_type: str,
|
|
3636
|
+
batch_id: str,
|
|
3637
|
+
session: Session = None,
|
|
3638
|
+
) -> Optional["IngestionJobs"]:
|
|
3639
|
+
"""Look for unique assay job with matching trial_id/assay_type/batch_id combination."""
|
|
3640
|
+
return (
|
|
3641
|
+
session.query(cls)
|
|
3642
|
+
.filter(
|
|
3643
|
+
cls.job_type == "assay",
|
|
3644
|
+
cls.trial_id == trial_id,
|
|
3645
|
+
cls.assay_type == assay_type,
|
|
3646
|
+
cls.batch_id == batch_id,
|
|
3647
|
+
)
|
|
3648
|
+
.first()
|
|
3649
|
+
)
|
|
3650
|
+
|
|
3651
|
+
@classmethod
|
|
3652
|
+
@with_default_session
|
|
3653
|
+
def next_assay_submission_id(cls, trial_id: str, assay_type: str, session: Session = None) -> str:
|
|
3654
|
+
"""
|
|
3655
|
+
Generate the next CIDC Submission ID for an assay job.
|
|
3656
|
+
|
|
3657
|
+
Format:
|
|
3658
|
+
<trial_id>-<assay_type>-<yyyymmdd> (first submission of the day)
|
|
3659
|
+
<trial_id>-<assay_type>-<yyyymmdd>-<#> (subsequent submissions on same day)
|
|
3660
|
+
|
|
3661
|
+
Uses only the most recent matching submission_id to determine the next suffix.
|
|
3662
|
+
"""
|
|
3663
|
+
today_str = datetime.now().strftime("%Y%m%d")
|
|
3664
|
+
base_submission_id = f"{trial_id}-{assay_type}-{today_str}"
|
|
3665
|
+
|
|
3666
|
+
# Get the most recent submission_id matching this prefix
|
|
3667
|
+
latest = (
|
|
3668
|
+
session.query(cls.submission_id)
|
|
3669
|
+
.filter(
|
|
3670
|
+
cls.trial_id == trial_id,
|
|
3671
|
+
cls.assay_type == assay_type,
|
|
3672
|
+
cls.submission_id.like(f"{base_submission_id}%"),
|
|
3673
|
+
)
|
|
3674
|
+
.order_by(cls._created.desc())
|
|
3675
|
+
.first()
|
|
3676
|
+
)
|
|
3677
|
+
|
|
3678
|
+
# No existing submission for this prefix -> start at 1
|
|
3679
|
+
if not latest or not latest[0]:
|
|
3680
|
+
return base_submission_id
|
|
3681
|
+
|
|
3682
|
+
last_id = latest[0]
|
|
3683
|
+
# Case 1: the latest is exactly the prefix (i.e., first submission today)
|
|
3684
|
+
if last_id == base_submission_id:
|
|
3685
|
+
return f"{base_submission_id}-2"
|
|
3686
|
+
|
|
3687
|
+
# Case 2: latest already has a suffix
|
|
3688
|
+
try:
|
|
3689
|
+
_, last_suffix = last_id.rsplit("-", 1)
|
|
3690
|
+
n = int(last_suffix)
|
|
3691
|
+
return f"{base_submission_id}-{n + 1}"
|
|
3692
|
+
except Exception as e:
|
|
3693
|
+
# If malformed, restart numbering for safety
|
|
3694
|
+
logger.error("Unexpected error parsing Submission ID in next_assay_submission_id: %s", e)
|
|
3695
|
+
return f"{base_submission_id}-2"
|
|
3696
|
+
|
|
3542
3697
|
|
|
3543
3698
|
class JobFileCategories(CommonColumns):
|
|
3544
3699
|
__tablename__ = "job_file_categories"
|
|
@@ -3613,6 +3768,7 @@ class CategoryDataElements(CommonColumns):
|
|
|
3613
3768
|
name = Column(String, nullable=False)
|
|
3614
3769
|
is_custom = Column(Boolean, nullable=False, default=False, server_default="false")
|
|
3615
3770
|
element_type = Column(String, nullable=False)
|
|
3771
|
+
data_type = Column(String, nullable=True)
|
|
3616
3772
|
cardinality = Column(String, nullable=True)
|
|
3617
3773
|
|
|
3618
3774
|
@classmethod
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import copy
|
|
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
|
|
6
|
+
|
|
7
|
+
from cidc_api.models.errors import ValueLocError
|
|
8
|
+
from functools import wraps
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Base(BaseModel):
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(
|
|
14
|
+
validate_assignment=True,
|
|
15
|
+
from_attributes=True,
|
|
16
|
+
extra="allow",
|
|
17
|
+
)
|
|
18
|
+
forced_validators: ClassVar = []
|
|
19
|
+
|
|
20
|
+
# Validates the new state and updates the object if valid
|
|
21
|
+
def update(self, **kwargs):
|
|
22
|
+
self.model_validate(self.__dict__ | kwargs)
|
|
23
|
+
self.__dict__.update(kwargs)
|
|
24
|
+
|
|
25
|
+
# CM that delays validation until all fields are applied.
|
|
26
|
+
# If validation fails the original fields are restored and the ValidationError is raised.
|
|
27
|
+
@contextmanager
|
|
28
|
+
def delay_validation(self):
|
|
29
|
+
original_dict = copy.deepcopy(self.__dict__)
|
|
30
|
+
self.model_config["validate_assignment"] = False
|
|
31
|
+
try:
|
|
32
|
+
yield
|
|
33
|
+
finally:
|
|
34
|
+
self.model_config["validate_assignment"] = True
|
|
35
|
+
try:
|
|
36
|
+
self.model_validate(self.__dict__)
|
|
37
|
+
except:
|
|
38
|
+
self.__dict__.update(original_dict)
|
|
39
|
+
raise
|
|
40
|
+
|
|
41
|
+
# CM that delays validation until all fields are applied.
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def split_list(cls, val):
|
|
45
|
+
"""Listify fields that are multi-valued in input data, e.g. 'lung|kidney'"""
|
|
46
|
+
if type(val) == list:
|
|
47
|
+
return val
|
|
48
|
+
elif type(val) == str:
|
|
49
|
+
if not val:
|
|
50
|
+
return []
|
|
51
|
+
return val.split("|")
|
|
52
|
+
elif val == None:
|
|
53
|
+
return []
|
|
54
|
+
else:
|
|
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
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from .additional_treatment import AdditionalTreatment
|
|
2
|
+
from .adverse_event import AdverseEvent
|
|
3
|
+
from .baseline_clinical_assessment import BaselineClinicalAssessment
|
|
4
|
+
from .comorbidity import Comorbidity
|
|
5
|
+
from .consent_group import ConsentGroup
|
|
6
|
+
from .demographic import Demographic
|
|
7
|
+
from .disease import Disease
|
|
8
|
+
from .exposure import Exposure
|
|
9
|
+
from .gvhd_diagnosis_acute import GVHDDiagnosisAcute
|
|
10
|
+
from .gvhd_diagnosis_chronic import GVHDDiagnosisChronic
|
|
11
|
+
from .gvhd_organ_acute import GVHDOrganAcute
|
|
12
|
+
from .gvhd_organ_chronic import GVHDOrganChronic
|
|
13
|
+
from .medical_history import MedicalHistory
|
|
14
|
+
from .other_malignancy import OtherMalignancy
|
|
15
|
+
from .participant import Participant
|
|
16
|
+
from .prior_treatment import PriorTreatment
|
|
17
|
+
from .radiotherapy_dose import RadiotherapyDose
|
|
18
|
+
from .response import Response
|
|
19
|
+
from .response_by_system import ResponseBySystem
|
|
20
|
+
from .specimen import Specimen
|
|
21
|
+
from .stem_cell_transplant import StemCellTransplant
|
|
22
|
+
from .surgery import Surgery
|
|
23
|
+
from .therapy_agent_dose import TherapyAgentDose
|
|
24
|
+
from .treatment import Treatment
|
|
25
|
+
from .trial import Trial
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AdditionalTreatment",
|
|
30
|
+
"AdverseEvent",
|
|
31
|
+
"BaselineClinicalAssessment",
|
|
32
|
+
"Comorbidity",
|
|
33
|
+
"ConsentGroup",
|
|
34
|
+
"Demographic",
|
|
35
|
+
"Disease",
|
|
36
|
+
"Exposure",
|
|
37
|
+
"GVHDDiagnosisAcute",
|
|
38
|
+
"GVHDOrganAcute",
|
|
39
|
+
"GVHDDiagnosisChronic",
|
|
40
|
+
"GVHDOrganChronic",
|
|
41
|
+
"MedicalHistory",
|
|
42
|
+
"OtherMalignancy",
|
|
43
|
+
"Participant",
|
|
44
|
+
"PriorTreatment",
|
|
45
|
+
"RadiotherapyDose",
|
|
46
|
+
"Response",
|
|
47
|
+
"ResponseBySystem",
|
|
48
|
+
"Specimen",
|
|
49
|
+
"StemCellTransplant",
|
|
50
|
+
"Surgery",
|
|
51
|
+
"TherapyAgentDose",
|
|
52
|
+
"Treatment",
|
|
53
|
+
"Trial",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
all_models = [globals()[cls_name] for cls_name in __all__]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pydantic import NonNegativeInt
|
|
2
|
+
from cidc_api.models.pydantic.base import Base
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AdditionalTreatment(Base):
|
|
6
|
+
__data_category__ = "additional_treatment"
|
|
7
|
+
__cardinality__ = "many"
|
|
8
|
+
|
|
9
|
+
# The unique internal identifier for the AdditionalTreatment record
|
|
10
|
+
additional_treatment_id: int | None = None
|
|
11
|
+
|
|
12
|
+
# The unique internal identifier for the associated Participant record
|
|
13
|
+
participant_id: str | None = None
|
|
14
|
+
|
|
15
|
+
# Number of days from the enrollment date to the first recorded administration or occurrence of the treatment modality.
|
|
16
|
+
additional_treatment_days_to_start: NonNegativeInt | None = None
|
|
17
|
+
|
|
18
|
+
# Number of days from the enrollment date to the last recorded administration or occurrence of the treatment modality.
|
|
19
|
+
additional_treatment_days_to_end: NonNegativeInt | None = None
|
|
20
|
+
|
|
21
|
+
# Description of the prior treatment such as its full generic name if it is a type of therapy agent, radiotherapy procedure
|
|
22
|
+
# name and location, or surgical procedure name and location.
|
|
23
|
+
additional_treatment_description: str
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from pydantic import NonNegativeInt
|
|
2
|
+
|
|
3
|
+
from cidc_api.models.pydantic.base import Base
|
|
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
|
+
from cidc_api.models.types import (
|
|
8
|
+
CTCAEEventTerm,
|
|
9
|
+
CTCAEEventCode,
|
|
10
|
+
SeverityGradeSystem,
|
|
11
|
+
SeverityGradeSystemVersion,
|
|
12
|
+
SeverityGrade,
|
|
13
|
+
SystemOrganClass,
|
|
14
|
+
AttributionCause,
|
|
15
|
+
AttributionLikelihood,
|
|
16
|
+
YNU,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@forced_validators
|
|
21
|
+
class AdverseEvent(Base):
|
|
22
|
+
__data_category__ = "adverse_event"
|
|
23
|
+
__cardinality__ = "many"
|
|
24
|
+
|
|
25
|
+
# The unique internal identifier of the adverse event
|
|
26
|
+
adverse_event_id: int | None = None
|
|
27
|
+
|
|
28
|
+
# The unique internal identifier of the associated participant
|
|
29
|
+
participant_id: str | None = None
|
|
30
|
+
|
|
31
|
+
# The unique internal identifier of the attributed treatment, if any
|
|
32
|
+
treatment_id: int | None = None
|
|
33
|
+
|
|
34
|
+
# Text that represents the Common Terminology Criteria for Adverse Events low level term name for an adverse event.
|
|
35
|
+
event_term: CTCAEEventTerm | None = None
|
|
36
|
+
|
|
37
|
+
# A MedDRA code mapped to a CTCAE low level name for an adverse event.
|
|
38
|
+
event_code: CTCAEEventCode | None = None
|
|
39
|
+
|
|
40
|
+
# System used to define and report adverse event severity grade.
|
|
41
|
+
severity_grade_system: SeverityGradeSystem
|
|
42
|
+
|
|
43
|
+
# The version of the adverse event grading system.
|
|
44
|
+
severity_grade_system_version: SeverityGradeSystemVersion
|
|
45
|
+
|
|
46
|
+
# Numerical grade indicating the severity of an adverse event.
|
|
47
|
+
severity_grade: SeverityGrade
|
|
48
|
+
|
|
49
|
+
# A brief description that sufficiently details the event.
|
|
50
|
+
event_other_specify: str | None = None
|
|
51
|
+
|
|
52
|
+
# The highest level of the MedDRA hierarchy, distinguished by anatomical or physiological system, etiology (disease origin) or purpose.
|
|
53
|
+
system_organ_class: SystemOrganClass | None = None
|
|
54
|
+
|
|
55
|
+
# Indicator to identify whether a participant exited the study prematurely due to the adverse event being described.
|
|
56
|
+
discontinuation_due_to_event: bool
|
|
57
|
+
|
|
58
|
+
# Days from enrollment date to date of onset of the adverse event.
|
|
59
|
+
days_to_onset_of_event: NonNegativeInt
|
|
60
|
+
|
|
61
|
+
# Days from enrollment date to date of resolution of the adverse event.
|
|
62
|
+
days_to_resolution_of_event: NonNegativeInt | None = None
|
|
63
|
+
|
|
64
|
+
# Indicates whether the adverse event was a serious adverse event (SAE).
|
|
65
|
+
serious_adverse_event: YNU
|
|
66
|
+
|
|
67
|
+
# Indicates whether the adverse event was a dose-limiting toxicity (DLT).
|
|
68
|
+
dose_limiting_toxicity: YNU
|
|
69
|
+
|
|
70
|
+
# Indicates if the adverse was attributable to the protocol as a whole or to an individual treatment.
|
|
71
|
+
attribution_cause: AttributionCause
|
|
72
|
+
|
|
73
|
+
# The code that indicates whether the adverse event is related to the treatment/intervention.
|
|
74
|
+
attribution_likelihood: AttributionLikelihood
|
|
75
|
+
|
|
76
|
+
# The individual therapy (therapy agent, radiotherapy, surgery, stem cell transplant) in the treatment that is attributed to the adverse event.
|
|
77
|
+
individual_therapy: str | None = None
|
|
78
|
+
|
|
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"
|
|
114
|
+
)
|
|
115
|
+
|
|
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
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from cidc_api.models.pydantic.base import Base
|
|
2
|
+
from cidc_api.models.types import ECOGScore, KarnofskyScore
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaselineClinicalAssessment(Base):
|
|
6
|
+
__data_category__ = "baseline_clinical_assessment"
|
|
7
|
+
__cardinality__ = "one"
|
|
8
|
+
|
|
9
|
+
# A unique internal identifier for the baseline clinical assessment
|
|
10
|
+
baseline_clinical_assessment_id: int | None = None
|
|
11
|
+
|
|
12
|
+
# The unique identifier for the associated participant
|
|
13
|
+
participant_id: str | None = None
|
|
14
|
+
|
|
15
|
+
# The numerical score that represents the functional capabilities of a participant at the
|
|
16
|
+
# enrollment date using the Eastern Cooperative Oncology Group Performance Status assessment.
|
|
17
|
+
# CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=88%20and%20ver_nr=5.1
|
|
18
|
+
ecog_score: ECOGScore | None = None
|
|
19
|
+
|
|
20
|
+
# Score from the Karnofsky Performance status scale, representing the functional capabilities of a participant
|
|
21
|
+
# at the enrollment date.
|
|
22
|
+
# CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2003853%20and%20ver_nr=4.2
|
|
23
|
+
karnofsky_score: KarnofskyScore | None = None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from cidc_api.models.pydantic.base import forced_validator, forced_validators
|
|
4
|
+
|
|
5
|
+
from cidc_api.models.errors import ValueLocError
|
|
6
|
+
from cidc_api.models.pydantic.base import Base
|
|
7
|
+
from cidc_api.models.types import ICD10CMCode, ICD10CMTerm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@forced_validators
|
|
11
|
+
class Comorbidity(Base):
|
|
12
|
+
__data_category__ = "comorbidity"
|
|
13
|
+
__cardinality__ = "many"
|
|
14
|
+
|
|
15
|
+
# The unique internal identifier for the comorbidity record
|
|
16
|
+
comorbidity_id: int | None = None
|
|
17
|
+
|
|
18
|
+
# The unique internal identifier for the associated MedicalHistory record
|
|
19
|
+
medical_history_id: int | None = None
|
|
20
|
+
|
|
21
|
+
# The diagnosis, in humans, as captured in the tenth version of the
|
|
22
|
+
# International Classification of Disease (ICD-10-CM, the disease code subset of ICD-10).
|
|
23
|
+
comorbidity_code: ICD10CMCode | None = None
|
|
24
|
+
|
|
25
|
+
# The words from the tenth version of the International Classification of Disease (ICD-10-CM,
|
|
26
|
+
# the disease subset of ICD-10) used to identify the diagnosis in humans.
|
|
27
|
+
comorbidity_term: ICD10CMTerm | None = None
|
|
28
|
+
|
|
29
|
+
# A descriptive string that names or briefly describes the comorbidity.
|
|
30
|
+
comorbidity_other: str | None = None
|
|
31
|
+
|
|
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",
|
|
43
|
+
)
|