nci-cidc-api-modules 1.2.29__py3-none-any.whl → 1.2.45__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 (148) hide show
  1. cidc_api/__init__.py +1 -0
  2. cidc_api/config/db.py +21 -1
  3. cidc_api/config/settings.py +1 -0
  4. cidc_api/models/__init__.py +0 -2
  5. cidc_api/models/data.py +17 -4
  6. cidc_api/models/db/stage1/__init__.py +56 -0
  7. cidc_api/models/db/stage1/additional_treatment_orm.py +22 -0
  8. cidc_api/models/db/stage1/adverse_event_orm.py +46 -0
  9. cidc_api/models/db/stage1/base_orm.py +7 -0
  10. cidc_api/models/db/stage1/baseline_clinical_assessment_orm.py +22 -0
  11. cidc_api/models/db/stage1/comorbidity_orm.py +23 -0
  12. cidc_api/models/db/stage1/consent_group_orm.py +32 -0
  13. cidc_api/models/db/stage1/demographic_orm.py +47 -0
  14. cidc_api/models/db/stage1/disease_orm.py +52 -0
  15. cidc_api/models/db/stage1/exposure_orm.py +22 -0
  16. cidc_api/models/db/stage1/gvhd_diagnosis_acute_orm.py +34 -0
  17. cidc_api/models/db/stage1/gvhd_diagnosis_chronic_orm.py +36 -0
  18. cidc_api/models/db/stage1/gvhd_organ_acute_orm.py +21 -0
  19. cidc_api/models/db/stage1/gvhd_organ_chronic_orm.py +21 -0
  20. cidc_api/models/db/stage1/medical_history_orm.py +30 -0
  21. cidc_api/models/db/stage1/other_malignancy_orm.py +29 -0
  22. cidc_api/models/db/stage1/participant_orm.py +77 -0
  23. cidc_api/models/db/stage1/prior_treatment_orm.py +29 -0
  24. cidc_api/models/db/stage1/radiotherapy_dose_orm.py +39 -0
  25. cidc_api/models/db/stage1/response_by_system_orm.py +30 -0
  26. cidc_api/models/db/stage1/response_orm.py +28 -0
  27. cidc_api/models/db/stage1/specimen_orm.py +46 -0
  28. cidc_api/models/db/stage1/stem_cell_transplant_orm.py +25 -0
  29. cidc_api/models/db/stage1/surgery_orm.py +27 -0
  30. cidc_api/models/db/stage1/therapy_agent_dose_orm.py +31 -0
  31. cidc_api/models/db/stage1/treatment_orm.py +38 -0
  32. cidc_api/models/db/stage1/trial_orm.py +35 -0
  33. cidc_api/models/db/stage2/additional_treatment_orm.py +8 -8
  34. cidc_api/models/db/stage2/administrative_person_orm.py +4 -4
  35. cidc_api/models/db/stage2/administrative_role_assignment_orm.py +4 -4
  36. cidc_api/models/db/stage2/adverse_event_orm.py +12 -13
  37. cidc_api/models/db/stage2/arm_orm.py +3 -3
  38. cidc_api/models/db/stage2/base_orm.py +7 -0
  39. cidc_api/models/db/stage2/baseline_clinical_assessment_orm.py +6 -7
  40. cidc_api/models/db/stage2/cohort_orm.py +3 -3
  41. cidc_api/models/db/stage2/comorbidity_orm.py +7 -8
  42. cidc_api/models/db/stage2/consent_group_orm.py +5 -4
  43. cidc_api/models/db/stage2/contact_orm.py +16 -20
  44. cidc_api/models/db/stage2/demographic_orm.py +11 -8
  45. cidc_api/models/db/stage2/disease_orm.py +13 -14
  46. cidc_api/models/db/stage2/exposure_orm.py +5 -4
  47. cidc_api/models/db/stage2/file_orm.py +6 -9
  48. cidc_api/models/db/stage2/gvhd_diagnosis_acute_orm.py +5 -4
  49. cidc_api/models/db/stage2/gvhd_diagnosis_chronic_orm.py +5 -6
  50. cidc_api/models/db/stage2/gvhd_organ_acute_orm.py +4 -3
  51. cidc_api/models/db/stage2/gvhd_organ_chronic_orm.py +4 -3
  52. cidc_api/models/db/stage2/institution_orm.py +7 -7
  53. cidc_api/models/db/stage2/medical_history_orm.py +10 -9
  54. cidc_api/models/db/stage2/other_clinical_endpoint_orm.py +8 -12
  55. cidc_api/models/db/stage2/other_malignancy_orm.py +10 -11
  56. cidc_api/models/db/stage2/participant_orm.py +28 -28
  57. cidc_api/models/db/stage2/prior_treatment_orm.py +15 -14
  58. cidc_api/models/db/stage2/publication_orm.py +9 -11
  59. cidc_api/models/db/stage2/radiotherapy_dose_orm.py +9 -9
  60. cidc_api/models/db/stage2/response_by_system_orm.py +5 -3
  61. cidc_api/models/db/stage2/response_orm.py +6 -5
  62. cidc_api/models/db/stage2/shipment_orm.py +17 -17
  63. cidc_api/models/db/stage2/shipment_specimen_orm.py +4 -4
  64. cidc_api/models/db/stage2/specimen_orm.py +7 -6
  65. cidc_api/models/db/stage2/stem_cell_transplant_orm.py +7 -7
  66. cidc_api/models/db/stage2/surgery_orm.py +7 -7
  67. cidc_api/models/db/stage2/therapy_agent_dose_orm.py +8 -8
  68. cidc_api/models/db/stage2/treatment_orm.py +16 -15
  69. cidc_api/models/db/stage2/trial_orm.py +34 -33
  70. cidc_api/models/files/facets.py +4 -0
  71. cidc_api/models/models.py +154 -9
  72. cidc_api/models/pydantic/{stage2/base.py → base.py} +19 -1
  73. cidc_api/models/pydantic/stage1/__init__.py +56 -0
  74. cidc_api/models/pydantic/stage1/additional_treatment.py +23 -0
  75. cidc_api/models/pydantic/stage1/adverse_event.py +100 -0
  76. cidc_api/models/pydantic/stage1/baseline_clinical_assessment.py +23 -0
  77. cidc_api/models/pydantic/stage1/comorbidity.py +36 -0
  78. cidc_api/models/pydantic/stage1/consent_group.py +30 -0
  79. cidc_api/models/pydantic/stage1/demographic.py +123 -0
  80. cidc_api/models/pydantic/stage1/disease.py +158 -0
  81. cidc_api/models/pydantic/stage1/exposure.py +32 -0
  82. cidc_api/models/pydantic/stage1/gvhd_diagnosis_acute.py +33 -0
  83. cidc_api/models/pydantic/stage1/gvhd_diagnosis_chronic.py +32 -0
  84. cidc_api/models/pydantic/stage1/gvhd_organ_acute.py +22 -0
  85. cidc_api/models/pydantic/stage1/gvhd_organ_chronic.py +23 -0
  86. cidc_api/models/pydantic/stage1/medical_history.py +36 -0
  87. cidc_api/models/pydantic/stage1/other_malignancy.py +49 -0
  88. cidc_api/models/pydantic/stage1/participant.py +51 -0
  89. cidc_api/models/pydantic/stage1/prior_treatment.py +45 -0
  90. cidc_api/models/pydantic/stage1/radiotherapy_dose.py +79 -0
  91. cidc_api/models/pydantic/stage1/response.py +65 -0
  92. cidc_api/models/pydantic/stage1/response_by_system.py +112 -0
  93. cidc_api/models/pydantic/stage1/specimen.py +31 -0
  94. cidc_api/models/pydantic/stage1/stem_cell_transplant.py +35 -0
  95. cidc_api/models/pydantic/stage1/surgery.py +49 -0
  96. cidc_api/models/pydantic/stage1/therapy_agent_dose.py +67 -0
  97. cidc_api/models/pydantic/stage1/treatment.py +50 -0
  98. cidc_api/models/pydantic/stage1/trial.py +45 -0
  99. cidc_api/models/pydantic/stage2/additional_treatment.py +5 -5
  100. cidc_api/models/pydantic/stage2/administrative_person.py +1 -1
  101. cidc_api/models/pydantic/stage2/administrative_role_assignment.py +2 -2
  102. cidc_api/models/pydantic/stage2/adverse_event.py +2 -2
  103. cidc_api/models/pydantic/stage2/arm.py +2 -2
  104. cidc_api/models/pydantic/stage2/baseline_clinical_assessment.py +2 -2
  105. cidc_api/models/pydantic/stage2/cohort.py +1 -1
  106. cidc_api/models/pydantic/stage2/comorbidity.py +1 -1
  107. cidc_api/models/pydantic/stage2/consent_group.py +2 -2
  108. cidc_api/models/pydantic/stage2/contact.py +1 -1
  109. cidc_api/models/pydantic/stage2/demographic.py +27 -18
  110. cidc_api/models/pydantic/stage2/disease.py +33 -19
  111. cidc_api/models/pydantic/stage2/exposure.py +3 -3
  112. cidc_api/models/pydantic/stage2/file.py +2 -2
  113. cidc_api/models/pydantic/stage2/gvhd_diagnosis_acute.py +2 -2
  114. cidc_api/models/pydantic/stage2/gvhd_diagnosis_chronic.py +2 -2
  115. cidc_api/models/pydantic/stage2/gvhd_organ_acute.py +1 -1
  116. cidc_api/models/pydantic/stage2/gvhd_organ_chronic.py +1 -1
  117. cidc_api/models/pydantic/stage2/institution.py +1 -1
  118. cidc_api/models/pydantic/stage2/medical_history.py +2 -2
  119. cidc_api/models/pydantic/stage2/other_clinical_endpoint.py +1 -1
  120. cidc_api/models/pydantic/stage2/other_malignancy.py +12 -8
  121. cidc_api/models/pydantic/stage2/participant.py +10 -6
  122. cidc_api/models/pydantic/stage2/prior_treatment.py +14 -23
  123. cidc_api/models/pydantic/stage2/publication.py +2 -2
  124. cidc_api/models/pydantic/stage2/radiotherapy_dose.py +2 -2
  125. cidc_api/models/pydantic/stage2/response.py +5 -11
  126. cidc_api/models/pydantic/stage2/response_by_system.py +10 -7
  127. cidc_api/models/pydantic/stage2/shipment.py +2 -2
  128. cidc_api/models/pydantic/stage2/shipment_specimen.py +1 -1
  129. cidc_api/models/pydantic/stage2/specimen.py +8 -5
  130. cidc_api/models/pydantic/stage2/stem_cell_transplant.py +2 -2
  131. cidc_api/models/pydantic/stage2/surgery.py +1 -1
  132. cidc_api/models/pydantic/stage2/therapy_agent_dose.py +1 -1
  133. cidc_api/models/pydantic/stage2/treatment.py +2 -2
  134. cidc_api/models/pydantic/stage2/trial.py +19 -15
  135. cidc_api/models/types.py +45 -42
  136. cidc_api/shared/assay_handling.py +68 -0
  137. cidc_api/shared/auth.py +5 -5
  138. cidc_api/shared/file_handling.py +16 -4
  139. cidc_api/shared/gcloud_client.py +78 -16
  140. cidc_api/shared/utils.py +18 -9
  141. cidc_api/telemetry.py +101 -0
  142. {nci_cidc_api_modules-1.2.29.dist-info → nci_cidc_api_modules-1.2.45.dist-info}/METADATA +25 -14
  143. nci_cidc_api_modules-1.2.45.dist-info/RECORD +165 -0
  144. cidc_api/models/db/base_orm.py +0 -25
  145. nci_cidc_api_modules-1.2.29.dist-info/RECORD +0 -109
  146. {nci_cidc_api_modules-1.2.29.dist-info → nci_cidc_api_modules-1.2.45.dist-info}/WHEEL +0 -0
  147. {nci_cidc_api_modules-1.2.29.dist-info → nci_cidc_api_modules-1.2.45.dist-info}/licenses/LICENSE +0 -0
  148. {nci_cidc_api_modules-1.2.29.dist-info → nci_cidc_api_modules-1.2.45.dist-info}/top_level.txt +0 -0
@@ -1,60 +1,61 @@
1
+ from __future__ import annotations
1
2
  from datetime import datetime
2
- from typing import List, Optional
3
+ from typing import List
3
4
 
4
5
  from sqlalchemy import ForeignKey
5
6
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
7
  from sqlalchemy.types import JSON
7
8
 
8
- from cidc_api.models.db.base_orm import BaseORM
9
- from cidc_api.models.types import TrialStatus, AssayType, TrialOrganization, TrialFundingAgency
9
+ from cidc_api.models.db.stage2.base_orm import BaseORM
10
+ from cidc_api.models.types import AssayType, TrialOrganization, TrialFundingAgency, AgeGroup, PrimaryPurposeType
10
11
 
11
12
 
12
13
  class TrialORM(BaseORM):
13
14
  __tablename__ = "trial"
14
15
  __repr_attrs__ = ["trial_id", "version"]
15
- __table_args__ = {"schema": "stage2"}
16
+ __data_category__ = "study"
16
17
 
17
18
  trial_id: Mapped[str] = mapped_column(primary_key=True)
18
19
  version: Mapped[str] = mapped_column(primary_key=True)
19
20
 
20
- nct_id: Mapped[Optional[str]]
21
- nci_id: Mapped[Optional[str]]
22
- trial_name: Mapped[Optional[str]]
23
- trial_type: Mapped[Optional[str]]
24
- trial_description: Mapped[Optional[str]]
25
- trial_organization: Mapped[Optional[TrialOrganization]]
26
- grant_or_affiliated_network: Mapped[Optional[TrialFundingAgency]]
27
- trial_status: Mapped[TrialStatus]
28
- biobank_institution_id: Mapped[Optional[int]]
29
- justification: Mapped[Optional[str]]
21
+ primary_endpoint: Mapped[str | None]
22
+ age_group: Mapped[List[AgeGroup]] = mapped_column(JSON, nullable=True)
23
+ study_population: Mapped[str | None]
24
+ nct_id: Mapped[str | None]
25
+ nci_id: Mapped[str | None]
26
+ trial_name: Mapped[str | None]
27
+ trial_type: Mapped[str | None]
28
+ trial_description: Mapped[str | None]
29
+ trial_organization: Mapped[TrialOrganization | None]
30
+ grant_or_affiliated_network: Mapped[TrialFundingAgency | None]
31
+ biobank_institution_id: Mapped[int | None]
32
+ justification: Mapped[str | None]
30
33
  dates_of_conduct_start: Mapped[datetime]
31
- dates_of_conduct_end: Mapped[Optional[datetime]]
32
- schema_file_id: Mapped[Optional[int]]
33
- biomarker_plan: Mapped[Optional[str]]
34
- data_sharing_plan: Mapped[Optional[str]]
35
- expected_assays: Mapped[Optional[List[AssayType]]] = mapped_column(JSON, nullable=True)
36
- is_liquid_tumor_trial: Mapped[bool]
37
- dbgap_study_accession: Mapped[Optional[str]]
34
+ dates_of_conduct_end: Mapped[datetime | None]
35
+ schema_file_id: Mapped[int | None]
36
+ biomarker_plan: Mapped[str | None]
37
+ data_sharing_plan: Mapped[str | None]
38
+ expected_assays: Mapped[List[AssayType]] = mapped_column(JSON, nullable=True)
39
+ primary_purpose_type: Mapped[PrimaryPurposeType]
40
+ dbgap_study_accession: Mapped[str | None]
38
41
 
39
- biobank: Mapped["InstitutionORM"] = relationship(back_populates="trial")
40
- schema: Mapped[Optional["FileORM"]] = relationship(back_populates="trial", viewonly=True)
41
- administrative_role_assignments: Mapped[List["AdministrativeRoleAssignmentORM"]] = relationship(
42
+ biobank: Mapped[InstitutionORM] = relationship(back_populates="trial")
43
+ schema: Mapped[FileORM | None] = relationship(back_populates="trial", viewonly=True)
44
+ administrative_role_assignments: Mapped[List[AdministrativeRoleAssignmentORM]] = relationship(
42
45
  back_populates="trial", cascade="all, delete", passive_deletes=True
43
46
  )
44
- arms: Mapped[List["ArmORM"]] = relationship(back_populates="trial", cascade="all, delete", passive_deletes=True)
45
- cohorts: Mapped[List["CohortORM"]] = relationship(
47
+ arms: Mapped[List[ArmORM]] = relationship(back_populates="trial", cascade="all, delete", passive_deletes=True)
48
+ cohorts: Mapped[List[CohortORM]] = relationship(back_populates="trial", cascade="all, delete", passive_deletes=True)
49
+ participants: Mapped[List[ParticipantORM]] = relationship(
46
50
  back_populates="trial", cascade="all, delete", passive_deletes=True
47
51
  )
48
- participants: Mapped[List["ParticipantORM"]] = relationship(
52
+ shipments: Mapped[List[ShipmentORM]] = relationship(
49
53
  back_populates="trial", cascade="all, delete", passive_deletes=True
50
54
  )
51
- shipments: Mapped[List["ShipmentORM"]] = relationship(
55
+ files: Mapped[List[FileORM]] = relationship(back_populates="trial", cascade="all, delete", passive_deletes=True)
56
+ publications: Mapped[List[PublicationORM]] = relationship(
52
57
  back_populates="trial", cascade="all, delete", passive_deletes=True
53
58
  )
54
- files: Mapped[List["FileORM"]] = relationship(back_populates="trial", cascade="all, delete", passive_deletes=True)
55
- publications: Mapped[List["PublicationORM"]] = relationship(
56
- back_populates="trial", cascade="all, delete", passive_deletes=True
57
- )
58
- consent_groups: Mapped[List["ConsentGroupORM"]] = relationship(
59
+ consent_groups: Mapped[List[ConsentGroupORM]] = relationship(
59
60
  back_populates="trial", cascade="all, delete", passive_deletes=True
60
61
  )
@@ -376,6 +376,10 @@ assay_facets: Facets = {
376
376
  "Channels": FacetConfig(["/maldi_glycan/channels.csv", "Channels csv file for MALDI Glycan run"]),
377
377
  "Tiff Zip": FacetConfig(["/maldi_glycan/tiff.zip", "Tiff zip for MALDI Glycan run"]),
378
378
  },
379
+ "TCRseq RNA": {
380
+ "Alpha Results": FacetConfig(["/tcrseq_rna/alpha.csv"]),
381
+ "Beta Results": FacetConfig(["/tcrseq_rna/beta.csv"]),
382
+ },
379
383
  "mIHC": {
380
384
  "Samples Report": FacetConfig(["/mihc/sample_report.csv"], "Samples report for mIHC run"),
381
385
  "Multitiffs": FacetConfig(["/mihc/multitiffs.tar.gz"], "Multi Tiffs file from mIHC run"),
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,8 @@ __all__ = [
35
36
  "ADMIN_FILE_CATEGORIES",
36
37
  "FINAL_JOB_STATUS",
37
38
  "INGESTION_JOB_STATUSES",
38
- "INGESTION_JOB_COLORS",
39
+ "ASSAY_JOB_COLORS",
40
+ "CLINICAL_JOB_COLORS",
39
41
  ]
40
42
 
41
43
  import hashlib
@@ -3261,6 +3263,7 @@ def upload_manifest_json(
3261
3263
  return manifest_upload.id
3262
3264
 
3263
3265
 
3266
+ MASTER_APPENDIX_A = "master_appendix_a"
3264
3267
  TRIAL_APPENDIX_A = "trial_appendix_a"
3265
3268
  REQUEST_LETTER = "request_letter"
3266
3269
  DETAILED_VALIDATION = "detailed_validation"
@@ -3360,6 +3363,16 @@ class PreprocessedFiles(CommonColumns):
3360
3363
  query = cls.add_job_filter(query, job_id)
3361
3364
  return query.all()
3362
3365
 
3366
+ @classmethod
3367
+ @with_default_session
3368
+ def get_latest_current_file(
3369
+ cls, file_category: str, job_id: int = None, session: Session = None
3370
+ ) -> Optional["PreprocessedFiles"]:
3371
+ """Return the latest 'current' file for the given category and job_id. Returns None if no file exists."""
3372
+ query = session.query(cls).filter_by(file_category=file_category, status="current")
3373
+ query = cls.add_job_filter(query, job_id)
3374
+ return query.order_by(cls.version.desc()).first()
3375
+
3363
3376
  @classmethod
3364
3377
  @with_default_session
3365
3378
  def get_file_by_category_and_version(
@@ -3399,6 +3412,7 @@ class PreprocessedFiles(CommonColumns):
3399
3412
  (cls.file_category == latest_subquery.c.file_category)
3400
3413
  & (cls._created == latest_subquery.c.latest_created),
3401
3414
  )
3415
+ .order_by(cls.file_category)
3402
3416
  .all()
3403
3417
  )
3404
3418
  return latest_files
@@ -3435,7 +3449,7 @@ INGESTION_JOB_STATUSES = [
3435
3449
  ]
3436
3450
 
3437
3451
  # Business decision to pass hex codes from the backend though that should be done by the front end...
3438
- INGESTION_JOB_COLORS = {
3452
+ CLINICAL_JOB_COLORS = {
3439
3453
  "DRAFT": "",
3440
3454
  "INITIAL SUBMISSION": "#ACCAD7",
3441
3455
  "VALIDATION REVIEW": "#DABE90",
@@ -3443,6 +3457,13 @@ INGESTION_JOB_COLORS = {
3443
3457
  "INGESTION": "#8FCEC7",
3444
3458
  "PUBLISHED": "#90D9E6",
3445
3459
  }
3460
+ ASSAY_JOB_COLORS = {
3461
+ "INITIAL SUBMISSION": "#43807E",
3462
+ "VALIDATION REVIEW": "#906F3F",
3463
+ "REVISION SUBMISSION": "#95358A",
3464
+ "INGESTION": "#542C88",
3465
+ "PUBLISHED": "#1C81A0",
3466
+ }
3446
3467
  # TODO If have "CANCELLED" concept or other final status, add here
3447
3468
  FINAL_JOB_STATUS = ["PUBLISHED"]
3448
3469
  TRIAL_APPENDIX_A_CELL_THAT_ENDS_THE_HEADER = "Data Category"
@@ -3464,11 +3485,45 @@ class IngestionJobs(CommonColumns):
3464
3485
  pending = Column(Boolean, nullable=False, default=False)
3465
3486
  start_date = Column(DateTime, nullable=True)
3466
3487
  error_status = Column(String, nullable=True)
3488
+ job_type = Column(String, nullable=False, default="clinical")
3489
+ assay_type = Column(String, nullable=True)
3490
+ batch_id = Column(String, nullable=True)
3491
+ submission_id = Column(String, nullable=True)
3492
+ intake_path = Column(String, nullable=True)
3493
+ uploader_email = Column(String, nullable=True)
3494
+ is_template_downloaded = Column(Boolean, nullable=True)
3467
3495
 
3468
3496
  @staticmethod
3469
3497
  @with_default_session
3470
- def create(trial_id: str, status: str, version: int, pending: Boolean = False, session: Session = None):
3471
- new_job = IngestionJobs(trial_id=trial_id, status=status, version=version, pending=pending)
3498
+ def create(
3499
+ trial_id: str,
3500
+ status: str,
3501
+ version: int,
3502
+ error_status: str = None,
3503
+ pending: Boolean = False,
3504
+ job_type: str = "clinical",
3505
+ assay_type: str = None,
3506
+ batch_id: str = None,
3507
+ submission_id: str = None,
3508
+ intake_path: str = None,
3509
+ start_date: datetime = None,
3510
+ uploader_email: str = None,
3511
+ session: Session = None,
3512
+ ):
3513
+ new_job = IngestionJobs(
3514
+ trial_id=trial_id,
3515
+ status=status,
3516
+ error_status=error_status,
3517
+ version=version,
3518
+ pending=pending,
3519
+ job_type=job_type,
3520
+ assay_type=assay_type,
3521
+ batch_id=batch_id,
3522
+ submission_id=submission_id,
3523
+ intake_path=intake_path,
3524
+ start_date=start_date,
3525
+ uploader_email=uploader_email,
3526
+ )
3472
3527
  new_job.insert(session=session)
3473
3528
  return new_job
3474
3529
 
@@ -3493,29 +3548,43 @@ class IngestionJobs(CommonColumns):
3493
3548
 
3494
3549
  @classmethod
3495
3550
  @with_default_session
3496
- def get_jobs_by_trial(cls, trial_id: str, session: Session = None) -> list["IngestionJobs"]:
3497
- return session.query(cls).filter(cls.trial_id == trial_id).order_by(cls.version.desc()).all()
3551
+ def get_jobs_by_trial(
3552
+ cls, trial_id: str, job_type: str = "clinical", session: Session = None
3553
+ ) -> list["IngestionJobs"]:
3554
+ return (
3555
+ session.query(cls)
3556
+ .filter(cls.trial_id == trial_id, cls.job_type == job_type)
3557
+ .order_by(cls.version.desc())
3558
+ .all()
3559
+ )
3498
3560
 
3499
3561
  @classmethod
3500
3562
  @with_default_session
3501
- def get_open_job_by_trial(cls, trial_id: str, session: Session = None) -> Optional["IngestionJobs"]:
3563
+ def get_open_job_by_trial(
3564
+ cls, trial_id: str, job_type: str = "clinical", session: Session = None
3565
+ ) -> Optional["IngestionJobs"]:
3502
3566
  """Return the open job for a given trial if it exists."""
3503
3567
  return (
3504
3568
  session.query(cls)
3505
3569
  .filter(
3506
3570
  cls.trial_id == trial_id,
3571
+ cls.job_type == job_type,
3507
3572
  cls.status.notin_(FINAL_JOB_STATUS),
3508
3573
  )
3509
3574
  .order_by(cls._created.desc())
3510
3575
  .first()
3511
3576
  )
3512
3577
 
3578
+ @classmethod
3579
+ def get_jobs_for_user(cls, user: Users, job_type: str = None) -> list["IngestionJobs"]:
3580
+ return cls.get_assay_jobs_for_user(user) if job_type == "assay" else cls.get_clinical_jobs_for_user(user)
3581
+
3513
3582
  @classmethod
3514
3583
  @with_default_session
3515
- def get_open_jobs_for_user(cls, user: Users, session: Session = None) -> list["IngestionJobs"]:
3584
+ def get_clinical_jobs_for_user(cls, user: Users, session: Session = None) -> list["IngestionJobs"]:
3516
3585
  if user.role not in [CIDCRole.ADMIN.value, CIDCRole.CLINICAL_TRIAL_USER.value]:
3517
3586
  return []
3518
- job_query = session.query(cls).filter(cls.status.notin_(["DRAFT"]))
3587
+ job_query = session.query(cls).filter(cls.status.notin_(["DRAFT"]), cls.job_type == "clinical")
3519
3588
  if (
3520
3589
  user.role != CIDCRole.ADMIN.value
3521
3590
  and not session.query(Permissions)
@@ -3538,6 +3607,81 @@ class IngestionJobs(CommonColumns):
3538
3607
  job_query = job_query.filter(cls.trial_id.in_(map(lambda x: x.trial_id, authorized_trials)))
3539
3608
  return job_query.order_by(cls._created.desc()).all()
3540
3609
 
3610
+ @classmethod
3611
+ @with_default_session
3612
+ def get_assay_jobs_for_user(cls, user: Users, session: Session = None) -> list["IngestionJobs"]:
3613
+ # TODO allow more than just Admin role and get authorized trials based on permissions
3614
+ if user.role not in [CIDCRole.ADMIN.value]:
3615
+ return []
3616
+ return session.query(cls).filter(cls.job_type == "assay").order_by(cls._created.desc()).all()
3617
+
3618
+ @classmethod
3619
+ @with_default_session
3620
+ def get_unique_assay_job(
3621
+ cls,
3622
+ trial_id: str,
3623
+ assay_type: str,
3624
+ batch_id: str,
3625
+ session: Session = None,
3626
+ ) -> Optional["IngestionJobs"]:
3627
+ """Look for unique assay job with matching trial_id/assay_type/batch_id combination."""
3628
+ return (
3629
+ session.query(cls)
3630
+ .filter(
3631
+ cls.job_type == "assay",
3632
+ cls.trial_id == trial_id,
3633
+ cls.assay_type == assay_type,
3634
+ cls.batch_id == batch_id,
3635
+ )
3636
+ .first()
3637
+ )
3638
+
3639
+ @classmethod
3640
+ @with_default_session
3641
+ def next_assay_submission_id(cls, trial_id: str, assay_type: str, session: Session = None) -> str:
3642
+ """
3643
+ Generate the next CIDC Submission ID for an assay job.
3644
+
3645
+ Format:
3646
+ <trial_id>-<assay_type>-<yyyymmdd> (first submission of the day)
3647
+ <trial_id>-<assay_type>-<yyyymmdd>-<#> (subsequent submissions on same day)
3648
+
3649
+ Uses only the most recent matching submission_id to determine the next suffix.
3650
+ """
3651
+ today_str = datetime.now().strftime("%Y%m%d")
3652
+ base_submission_id = f"{trial_id}-{assay_type}-{today_str}"
3653
+
3654
+ # Get the most recent submission_id matching this prefix
3655
+ latest = (
3656
+ session.query(cls.submission_id)
3657
+ .filter(
3658
+ cls.trial_id == trial_id,
3659
+ cls.assay_type == assay_type,
3660
+ cls.submission_id.like(f"{base_submission_id}%"),
3661
+ )
3662
+ .order_by(cls._created.desc())
3663
+ .first()
3664
+ )
3665
+
3666
+ # No existing submission for this prefix -> start at 1
3667
+ if not latest or not latest[0]:
3668
+ return base_submission_id
3669
+
3670
+ last_id = latest[0]
3671
+ # Case 1: the latest is exactly the prefix (i.e., first submission today)
3672
+ if last_id == base_submission_id:
3673
+ return f"{base_submission_id}-2"
3674
+
3675
+ # Case 2: latest already has a suffix
3676
+ try:
3677
+ _, last_suffix = last_id.rsplit("-", 1)
3678
+ n = int(last_suffix)
3679
+ return f"{base_submission_id}-{n + 1}"
3680
+ except Exception as e:
3681
+ # If malformed, restart numbering for safety
3682
+ logger.error("Unexpected error parsing Submission ID in next_assay_submission_id: %s", e)
3683
+ return f"{base_submission_id}-2"
3684
+
3541
3685
 
3542
3686
  class JobFileCategories(CommonColumns):
3543
3687
  __tablename__ = "job_file_categories"
@@ -3612,6 +3756,7 @@ class CategoryDataElements(CommonColumns):
3612
3756
  name = Column(String, nullable=False)
3613
3757
  is_custom = Column(Boolean, nullable=False, default=False, server_default="false")
3614
3758
  element_type = Column(String, nullable=False)
3759
+ data_type = Column(String, nullable=True)
3615
3760
  cardinality = Column(String, nullable=True)
3616
3761
 
3617
3762
  @classmethod
@@ -6,7 +6,11 @@ import copy
6
6
 
7
7
  class Base(BaseModel):
8
8
 
9
- model_config = ConfigDict(validate_assignment=True, from_attributes=True)
9
+ model_config = ConfigDict(
10
+ validate_assignment=True,
11
+ from_attributes=True,
12
+ extra="allow",
13
+ )
10
14
 
11
15
  # Validates the new state and updates the object if valid
12
16
  def update(self, **kwargs):
@@ -28,3 +32,17 @@ class Base(BaseModel):
28
32
  except:
29
33
  self.__dict__.update(original_dict)
30
34
  raise
35
+
36
+ @classmethod
37
+ def split_list(cls, val):
38
+ """Listify fields that are multi-valued in input data, e.g. 'lung|kidney'"""
39
+ if type(val) == list:
40
+ return val
41
+ elif type(val) == str:
42
+ if not val:
43
+ return []
44
+ return val.split("|")
45
+ elif val == None:
46
+ return []
47
+ else:
48
+ raise ValueError("Field value must be string or list")
@@ -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,100 @@
1
+ from typing import Self
2
+
3
+ from pydantic import NonNegativeInt, model_validator
4
+
5
+ from cidc_api.models.pydantic.base import Base
6
+ from cidc_api.reference.ctcae import is_ctcae_other_term
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
+ class AdverseEvent(Base):
21
+ __data_category__ = "adverse_event"
22
+ __cardinality__ = "many"
23
+
24
+ # The unique internal identifier of the adverse event
25
+ adverse_event_id: int | None = None
26
+
27
+ # The unique internal identifier of the associated participant
28
+ participant_id: str | None = None
29
+
30
+ # The unique internal identifier of the attributed treatment, if any
31
+ treatment_id: int | None = None
32
+
33
+ # Text that represents the Common Terminology Criteria for Adverse Events low level term name for an adverse event.
34
+ event_term: CTCAEEventTerm | None = None
35
+
36
+ # A MedDRA code mapped to a CTCAE low level name for an adverse event.
37
+ event_code: CTCAEEventCode | None = None
38
+
39
+ # System used to define and report adverse event severity grade.
40
+ severity_grade_system: SeverityGradeSystem
41
+
42
+ # The version of the adverse event grading system.
43
+ severity_grade_system_version: SeverityGradeSystemVersion
44
+
45
+ # Numerical grade indicating the severity of an adverse event.
46
+ severity_grade: SeverityGrade
47
+
48
+ # A brief description that sufficiently details the event.
49
+ event_other_specify: str | None = None
50
+
51
+ # The highest level of the MedDRA hierarchy, distinguished by anatomical or physiological system, etiology (disease origin) or purpose.
52
+ system_organ_class: SystemOrganClass | None = None
53
+
54
+ # Indicator to identify whether a participant exited the study prematurely due to the adverse event being described.
55
+ discontinuation_due_to_event: bool
56
+
57
+ # Days from enrollment date to date of onset of the adverse event.
58
+ days_to_onset_of_event: NonNegativeInt
59
+
60
+ # Days from enrollment date to date of resolution of the adverse event.
61
+ days_to_resolution_of_event: NonNegativeInt | None = None
62
+
63
+ # Indicates whether the adverse event was a serious adverse event (SAE).
64
+ serious_adverse_event: YNU
65
+
66
+ # Indicates whether the adverse event was a dose-limiting toxicity (DLT).
67
+ dose_limiting_toxicity: YNU
68
+
69
+ # Indicates if the adverse was attributable to the protocol as a whole or to an individual treatment.
70
+ attribution_cause: AttributionCause
71
+
72
+ # The code that indicates whether the adverse event is related to the treatment/intervention.
73
+ attribution_likelihood: AttributionLikelihood
74
+
75
+ # The individual therapy (therapy agent, radiotherapy, surgery, stem cell transplant) in the treatment that is attributed to the adverse event.
76
+ individual_therapy: str | None = None
77
+
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'
93
+ )
94
+ return self
95
+
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
@@ -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,36 @@
1
+ from typing import Self
2
+
3
+ from pydantic import model_validator
4
+
5
+ from cidc_api.models.pydantic.base import Base
6
+ from cidc_api.models.types import ICD10CMCode, ICD10CMTerm
7
+
8
+
9
+ class Comorbidity(Base):
10
+ __data_category__ = "comorbidity"
11
+ __cardinality__ = "many"
12
+
13
+ # The unique internal identifier for the comorbidity record
14
+ comorbidity_id: int | None = None
15
+
16
+ # The unique internal identifier for the associated MedicalHistory record
17
+ medical_history_id: int | None = None
18
+
19
+ # The diagnosis, in humans, as captured in the tenth version of the
20
+ # International Classification of Disease (ICD-10-CM, the disease code subset of ICD-10).
21
+ comorbidity_code: ICD10CMCode | None = None
22
+
23
+ # The words from the tenth version of the International Classification of Disease (ICD-10-CM,
24
+ # the disease subset of ICD-10) used to identify the diagnosis in humans.
25
+ comorbidity_term: ICD10CMTerm | None = None
26
+
27
+ # A descriptive string that names or briefly describes the comorbidity.
28
+ comorbidity_other: str | None = None
29
+
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".'
35
+ )
36
+ return self
@@ -0,0 +1,30 @@
1
+ from pydantic import NonNegativeInt
2
+
3
+ from cidc_api.models.pydantic.base import Base
4
+
5
+
6
+ class ConsentGroup(Base):
7
+ __data_category__ = "consent_group"
8
+ __cardinality__ = "one"
9
+
10
+ # The unique internal identifier for the consent group record
11
+ consent_group_id: int | None = None
12
+
13
+ # The unique internal identifier for the associated Trial record
14
+ trial_id: str | None = None
15
+
16
+ # The version number of the trial dataset
17
+ version: str | None = None
18
+
19
+ # An abbreviated name for the consent group
20
+ consent_group_short_name: str
21
+
22
+ # The words or acronym which describe a set of study participants
23
+ # who have signed the same consent agreement and that will be included in the dbGaP repository.
24
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=14534329%20and%20ver_nr=1.00
25
+ consent_group_name: str
26
+
27
+ # A numeral or string of numerals used to identify the set of study participants who have signed the same consent
28
+ # agreement and that will be included in the dbGaP repository.
29
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=14534330%20and%20ver_nr=1.00
30
+ consent_group_number: NonNegativeInt