nci-cidc-api-modules 1.2.16__py3-none-any.whl → 1.2.25__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 (91) hide show
  1. cidc_api/config/db.py +1 -3
  2. cidc_api/models/__init__.py +2 -0
  3. cidc_api/models/data.py +15 -0
  4. cidc_api/models/db/base_orm.py +25 -0
  5. cidc_api/models/db/stage2/__init__.py +78 -0
  6. cidc_api/models/db/stage2/additional_treatment_orm.py +22 -0
  7. cidc_api/models/db/stage2/administrative_person_orm.py +25 -0
  8. cidc_api/models/db/stage2/administrative_role_assignment_orm.py +28 -0
  9. cidc_api/models/db/stage2/adverse_event_orm.py +47 -0
  10. cidc_api/models/db/stage2/arm_orm.py +23 -0
  11. cidc_api/models/db/stage2/baseline_clinical_assessment_orm.py +23 -0
  12. cidc_api/models/db/stage2/cohort_orm.py +23 -0
  13. cidc_api/models/db/stage2/comorbidity_orm.py +24 -0
  14. cidc_api/models/db/stage2/consent_group_orm.py +31 -0
  15. cidc_api/models/db/stage2/contact_orm.py +32 -0
  16. cidc_api/models/db/stage2/demographic_orm.py +44 -0
  17. cidc_api/models/db/stage2/disease_orm.py +53 -0
  18. cidc_api/models/db/stage2/exposure_orm.py +21 -0
  19. cidc_api/models/db/stage2/file_orm.py +38 -0
  20. cidc_api/models/db/stage2/gvhd_diagnosis_acute_orm.py +33 -0
  21. cidc_api/models/db/stage2/gvhd_diagnosis_chronic_orm.py +37 -0
  22. cidc_api/models/db/stage2/gvhd_organ_acute_orm.py +20 -0
  23. cidc_api/models/db/stage2/gvhd_organ_chronic_orm.py +20 -0
  24. cidc_api/models/db/stage2/institution_orm.py +33 -0
  25. cidc_api/models/db/stage2/medical_history_orm.py +29 -0
  26. cidc_api/models/db/stage2/other_clinical_endpoint_orm.py +28 -0
  27. cidc_api/models/db/stage2/other_malignancy_orm.py +30 -0
  28. cidc_api/models/db/stage2/participant_orm.py +79 -0
  29. cidc_api/models/db/stage2/prior_treatment_orm.py +28 -0
  30. cidc_api/models/db/stage2/publication_orm.py +31 -0
  31. cidc_api/models/db/stage2/radiotherapy_dose_orm.py +39 -0
  32. cidc_api/models/db/stage2/response_by_system_orm.py +28 -0
  33. cidc_api/models/db/stage2/response_orm.py +27 -0
  34. cidc_api/models/db/stage2/shipment_orm.py +47 -0
  35. cidc_api/models/db/stage2/shipment_specimen_orm.py +24 -0
  36. cidc_api/models/db/stage2/specimen_orm.py +100 -0
  37. cidc_api/models/db/stage2/stem_cell_transplant_orm.py +25 -0
  38. cidc_api/models/db/stage2/surgery_orm.py +27 -0
  39. cidc_api/models/db/stage2/therapy_agent_dose_orm.py +31 -0
  40. cidc_api/models/db/stage2/treatment_orm.py +39 -0
  41. cidc_api/models/db/stage2/trial_orm.py +60 -0
  42. cidc_api/models/files/facets.py +5 -0
  43. cidc_api/models/migrations.py +12 -39
  44. cidc_api/models/models.py +65 -14
  45. cidc_api/models/pydantic/stage2/__init__.py +78 -0
  46. cidc_api/models/pydantic/stage2/additional_treatment.py +23 -0
  47. cidc_api/models/pydantic/stage2/administrative_person.py +30 -0
  48. cidc_api/models/pydantic/stage2/administrative_role_assignment.py +16 -0
  49. cidc_api/models/pydantic/stage2/adverse_event.py +100 -0
  50. cidc_api/models/pydantic/stage2/arm.py +16 -0
  51. cidc_api/models/pydantic/stage2/base.py +30 -0
  52. cidc_api/models/pydantic/stage2/baseline_clinical_assessment.py +23 -0
  53. cidc_api/models/pydantic/stage2/cohort.py +16 -0
  54. cidc_api/models/pydantic/stage2/comorbidity.py +36 -0
  55. cidc_api/models/pydantic/stage2/consent_group.py +30 -0
  56. cidc_api/models/pydantic/stage2/contact.py +35 -0
  57. cidc_api/models/pydantic/stage2/demographic.py +114 -0
  58. cidc_api/models/pydantic/stage2/disease.py +144 -0
  59. cidc_api/models/pydantic/stage2/exposure.py +32 -0
  60. cidc_api/models/pydantic/stage2/file.py +44 -0
  61. cidc_api/models/pydantic/stage2/gvhd_diagnosis_acute.py +33 -0
  62. cidc_api/models/pydantic/stage2/gvhd_diagnosis_chronic.py +32 -0
  63. cidc_api/models/pydantic/stage2/gvhd_organ_acute.py +22 -0
  64. cidc_api/models/pydantic/stage2/gvhd_organ_chronic.py +23 -0
  65. cidc_api/models/pydantic/stage2/institution.py +10 -0
  66. cidc_api/models/pydantic/stage2/medical_history.py +36 -0
  67. cidc_api/models/pydantic/stage2/other_clinical_endpoint.py +32 -0
  68. cidc_api/models/pydantic/stage2/other_malignancy.py +45 -0
  69. cidc_api/models/pydantic/stage2/participant.py +47 -0
  70. cidc_api/models/pydantic/stage2/prior_treatment.py +52 -0
  71. cidc_api/models/pydantic/stage2/publication.py +37 -0
  72. cidc_api/models/pydantic/stage2/radiotherapy_dose.py +79 -0
  73. cidc_api/models/pydantic/stage2/response.py +71 -0
  74. cidc_api/models/pydantic/stage2/response_by_system.py +109 -0
  75. cidc_api/models/pydantic/stage2/shipment.py +48 -0
  76. cidc_api/models/pydantic/stage2/shipment_specimen.py +15 -0
  77. cidc_api/models/pydantic/stage2/specimen.py +211 -0
  78. cidc_api/models/pydantic/stage2/stem_cell_transplant.py +35 -0
  79. cidc_api/models/pydantic/stage2/surgery.py +49 -0
  80. cidc_api/models/pydantic/stage2/therapy_agent_dose.py +67 -0
  81. cidc_api/models/pydantic/stage2/treatment.py +50 -0
  82. cidc_api/models/pydantic/stage2/trial.py +85 -0
  83. cidc_api/models/types.py +1439 -0
  84. cidc_api/shared/file_handling.py +37 -2
  85. cidc_api/shared/utils.py +4 -1
  86. {nci_cidc_api_modules-1.2.16.dist-info → nci_cidc_api_modules-1.2.25.dist-info}/METADATA +6 -4
  87. nci_cidc_api_modules-1.2.25.dist-info/RECORD +104 -0
  88. nci_cidc_api_modules-1.2.16.dist-info/RECORD +0 -26
  89. {nci_cidc_api_modules-1.2.16.dist-info → nci_cidc_api_modules-1.2.25.dist-info}/WHEEL +0 -0
  90. {nci_cidc_api_modules-1.2.16.dist-info → nci_cidc_api_modules-1.2.25.dist-info}/licenses/LICENSE +0 -0
  91. {nci_cidc_api_modules-1.2.16.dist-info → nci_cidc_api_modules-1.2.25.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,39 @@
1
+ from typing import List, Optional
2
+
3
+ from sqlalchemy import ForeignKey
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from cidc_api.models.db.base_orm import BaseORM
7
+ from cidc_api.models.types import YNU, OffTreatmentReason
8
+
9
+
10
+ class TreatmentORM(BaseORM):
11
+ __tablename__ = "treatment"
12
+ __repr_attrs__ = ["treatment_id", "participant_id", "treatment_description"]
13
+ __table_args__ = {"schema": "stage2"}
14
+
15
+ treatment_id: Mapped[int] = mapped_column(primary_key=True)
16
+ participant_id: Mapped[int] = mapped_column(ForeignKey("stage2.participant.participant_id", ondelete="CASCADE"))
17
+ arm_id: Mapped[Optional[int]] = mapped_column(ForeignKey("stage2.arm.arm_id", ondelete="CASCADE"))
18
+ cohort_id: Mapped[Optional[int]] = mapped_column(ForeignKey("stage2.cohort.cohort_id", ondelete="CASCADE"))
19
+
20
+ treatment_description: Mapped[str]
21
+ off_treatment: Mapped[YNU]
22
+ off_treatment_reason: Mapped[Optional[OffTreatmentReason]]
23
+ off_treatment_reason_other: Mapped[Optional[str]]
24
+
25
+ participant: Mapped["ParticipantORM"] = relationship(back_populates="treatments", cascade="all, delete")
26
+ arm: Mapped[Optional["ArmORM"]] = relationship(cascade="all, delete")
27
+ cohort: Mapped[Optional["CohortORM"]] = relationship(cascade="all, delete")
28
+ adverse_events: Mapped[List["AdverseEventORM"]] = relationship(back_populates="treatment", cascade="all, delete")
29
+
30
+ therapy_agent_doses: Mapped[List["TherapyAgentDoseORM"]] = relationship(
31
+ back_populates="treatment", cascade="all, delete"
32
+ )
33
+ radiotherapy_doses: Mapped[List["RadiotherapyDoseORM"]] = relationship(
34
+ back_populates="treatment", cascade="all, delete"
35
+ )
36
+ surgeries: Mapped[List["SurgeryORM"]] = relationship(back_populates="treatment", cascade="all, delete")
37
+ stem_cell_transplants: Mapped[List["StemCellTransplantORM"]] = relationship(
38
+ back_populates="treatment", cascade="all, delete"
39
+ )
@@ -0,0 +1,60 @@
1
+ from datetime import datetime
2
+ from typing import List, Optional
3
+
4
+ from sqlalchemy import ForeignKey
5
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
+ from sqlalchemy.types import JSON
7
+
8
+ from cidc_api.models.db.base_orm import BaseORM
9
+ from cidc_api.models.types import TrialStatus, AssayType, TrialOrganization, TrialFundingAgency
10
+
11
+
12
+ class TrialORM(BaseORM):
13
+ __tablename__ = "trial"
14
+ __repr_attrs__ = ["trial_id", "version"]
15
+ __table_args__ = {"schema": "stage2"}
16
+
17
+ trial_id: Mapped[str] = mapped_column(primary_key=True)
18
+ version: Mapped[str] = mapped_column(primary_key=True)
19
+
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]]
30
+ 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]]
38
+
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
+ back_populates="trial", cascade="all, delete", passive_deletes=True
43
+ )
44
+ arms: Mapped[List["ArmORM"]] = relationship(back_populates="trial", cascade="all, delete", passive_deletes=True)
45
+ cohorts: Mapped[List["CohortORM"]] = relationship(
46
+ back_populates="trial", cascade="all, delete", passive_deletes=True
47
+ )
48
+ participants: Mapped[List["ParticipantORM"]] = relationship(
49
+ back_populates="trial", cascade="all, delete", passive_deletes=True
50
+ )
51
+ shipments: Mapped[List["ShipmentORM"]] = relationship(
52
+ back_populates="trial", cascade="all, delete", passive_deletes=True
53
+ )
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
+ back_populates="trial", cascade="all, delete", passive_deletes=True
60
+ )
@@ -439,6 +439,11 @@ assay_facets: Facets = {
439
439
  "Analysis files for all samples run on the Olink platform in the trial.",
440
440
  ),
441
441
  },
442
+ "Olink HT": {
443
+ "Batch-Level Combined File": FacetConfig(["/olink_ht/batch_level_combined_file.parquet"]),
444
+ "Study-Level Combined File": FacetConfig(["/olink_ht/study_level_combined_file.parquet"]),
445
+ "Npx Run File": FacetConfig(["/olink_ht/npx_run_file.parquet"]),
446
+ },
442
447
  "IHC": {
443
448
  "Images": FacetConfig(["/ihc/ihc_image."]),
444
449
  "Combined Markers": FacetConfig(["csv|ihc marker combined"]),
@@ -91,15 +91,11 @@ def migration_session():
91
91
  session.close()
92
92
 
93
93
 
94
- def run_metadata_migration(
95
- metadata_migration: Callable[[dict], MigrationResult], use_upload_jobs_table: bool
96
- ):
94
+ def run_metadata_migration(metadata_migration: Callable[[dict], MigrationResult], use_upload_jobs_table: bool):
97
95
  """Migrate trial metadata, upload job patches, and downloadable files according to `metadata_migration`"""
98
96
  with migration_session() as (session, task_queue):
99
97
  try:
100
- _run_metadata_migration(
101
- metadata_migration, use_upload_jobs_table, task_queue, session
102
- )
98
+ _run_metadata_migration(metadata_migration, use_upload_jobs_table, task_queue, session)
103
99
  except:
104
100
  traceback.print_exc()
105
101
  raise
@@ -122,9 +118,7 @@ class ManifestUploads(CommonColumns):
122
118
  __tablename__ = "manifest_uploads"
123
119
 
124
120
 
125
- def _select_successful_assay_uploads(
126
- use_upload_jobs_table: bool, session: Session
127
- ) -> List[UploadJobs]:
121
+ def _select_successful_assay_uploads(use_upload_jobs_table: bool, session: Session) -> List[UploadJobs]:
128
122
  if use_upload_jobs_table:
129
123
  return (
130
124
  session.query(UploadJobs)
@@ -133,21 +127,12 @@ def _select_successful_assay_uploads(
133
127
  .all()
134
128
  )
135
129
 
136
- return (
137
- session.query(AssayUploads)
138
- .filter_by(status=UploadJobStatus.MERGE_COMPLETED.value)
139
- .with_for_update()
140
- .all()
141
- )
130
+ return session.query(AssayUploads).filter_by(status=UploadJobStatus.MERGE_COMPLETED.value).with_for_update().all()
142
131
 
143
132
 
144
- def _select_manifest_uploads(
145
- use_upload_jobs_table: bool, session: Session
146
- ) -> List[UploadJobs]:
133
+ def _select_manifest_uploads(use_upload_jobs_table: bool, session: Session) -> List[UploadJobs]:
147
134
  if use_upload_jobs_table:
148
- return (
149
- session.query(UploadJobs).filter_by(multifile=False).with_for_update().all()
150
- )
135
+ return session.query(UploadJobs).filter_by(multifile=False).with_for_update().all()
151
136
 
152
137
  return session.query(ManifestUploads).with_for_update().all()
153
138
 
@@ -188,21 +173,15 @@ def _run_metadata_migration(
188
173
 
189
174
  # Regenerate additional metadata from the migrated clinical trial
190
175
  # metadata object.
191
- print(
192
- f"Regenerating additional metadata for artifact with uuid {artifact['upload_placeholder']}"
193
- )
176
+ print(f"Regenerating additional metadata for artifact with uuid {artifact['upload_placeholder']}")
194
177
  artifact_path = uuid_path_map[artifact["upload_placeholder"]]
195
- df.additional_metadata = get_source(
196
- migration.result, artifact_path, skip_last=True
197
- )[1]
178
+ df.additional_metadata = get_source(migration.result, artifact_path, skip_last=True)[1]
198
179
 
199
180
  # If the GCS URI has changed, rename the blob
200
181
  # makes call to bucket.rename_blob
201
182
  new_gcs_uri = artifact["object_url"]
202
183
  if old_gcs_uri != new_gcs_uri:
203
- print(
204
- f"Encountered GCS data bucket artifact URI to update: {old_gcs_uri}"
205
- )
184
+ print(f"Encountered GCS data bucket artifact URI to update: {old_gcs_uri}")
206
185
  renamer = PieceOfWork(
207
186
  partial(
208
187
  rename_gcs_blob,
@@ -220,9 +199,7 @@ def _run_metadata_migration(
220
199
  gcs_tasks.schedule(renamer)
221
200
 
222
201
  # Migrate all assay upload successes
223
- successful_assay_uploads = _select_successful_assay_uploads(
224
- use_upload_jobs_table, session
225
- )
202
+ successful_assay_uploads = _select_successful_assay_uploads(use_upload_jobs_table, session)
226
203
  for upload in successful_assay_uploads:
227
204
  print(f"Running metadata migration for assay upload: {upload.id}")
228
205
  if use_upload_jobs_table:
@@ -248,9 +225,7 @@ def _run_metadata_migration(
248
225
  if old_target_uri in migration.file_updates:
249
226
  new_target_uri = migration.file_updates[old_target_uri]["object_url"]
250
227
  if old_target_uri != new_target_uri:
251
- print(
252
- f"Encountered GCS upload bucket artifact URI to update: {old_upload_uri}"
253
- )
228
+ print(f"Encountered GCS upload bucket artifact URI to update: {old_upload_uri}")
254
229
  new_upload_uri = "/".join([new_target_uri, upload_timestamp])
255
230
  renamer = PieceOfWork(
256
231
  partial(
@@ -325,7 +300,5 @@ def republish_artifact_uploads():
325
300
  with migration_session() as (session, _):
326
301
  files = session.query(DownloadableFiles).all()
327
302
  for f in files:
328
- print(
329
- f"Publishing to 'artifact_upload' topic for downloadable file with in bucket url {f.object_url}"
330
- )
303
+ print(f"Publishing to 'artifact_upload' topic for downloadable file with in bucket url {f.object_url}")
331
304
  publish_artifact_upload(f.object_url)
cidc_api/models/models.py CHANGED
@@ -96,7 +96,7 @@ from sqlalchemy import (
96
96
  String,
97
97
  Table,
98
98
  )
99
- from sqlalchemy.dialects.postgresql import JSONB, UUID
99
+ from sqlalchemy.dialects.postgresql import JSONB, UUID, CITEXT
100
100
  from sqlalchemy.engine import ResultProxy
101
101
  from sqlalchemy.exc import IntegrityError
102
102
  from sqlalchemy.ext.hybrid import hybrid_property
@@ -381,7 +381,7 @@ class Users(CommonColumns):
381
381
  last_n = Column(String)
382
382
  organization = Column(Enum(*ORGS, name="orgs"))
383
383
  approval_date = Column(DateTime)
384
- role = Column(Enum(*ROLES, name="role"))
384
+ role = Column(Enum(*ROLES, name="roles"))
385
385
  disabled = Column(Boolean, default=False, server_default="false")
386
386
 
387
387
  @validates("approval_date")
@@ -606,6 +606,22 @@ class Permissions(CommonColumns):
606
606
  unique=True,
607
607
  postgresql_where=file_group_id.isnot(None),
608
608
  ),
609
+ Index(
610
+ "unique_trial_id_upload_type_is_null_perms",
611
+ "granted_to_user",
612
+ "trial_id",
613
+ literal_column("(upload_type IS NULL)"),
614
+ unique=True,
615
+ postgresql_where="(upload_type IS NULL)",
616
+ ),
617
+ Index(
618
+ "unique_upload_type_trial_id_is_null_perms",
619
+ "granted_to_user",
620
+ literal_column("(trial_id IS NULL)"),
621
+ "upload_type",
622
+ unique=True,
623
+ postgresql_where="(trial_id IS NULL)",
624
+ ),
609
625
  )
610
626
 
611
627
  # Shorthand to make code related to trial- and upload-type-level permissions
@@ -2293,7 +2309,7 @@ class DownloadableFiles(CommonColumns):
2293
2309
  additional_metadata = Column(JSONB, nullable=False)
2294
2310
  # TODO rename upload_type, because we store manifests in there too.
2295
2311
  # NOTE: this column actually has type CITEXT.
2296
- upload_type = Column(String, nullable=False)
2312
+ upload_type = Column(CITEXT, nullable=False)
2297
2313
  md5_hash = Column(String, nullable=True)
2298
2314
  crc32c_hash = Column(String, nullable=True)
2299
2315
  trial_id = Column(String, nullable=False)
@@ -2313,7 +2329,7 @@ class DownloadableFiles(CommonColumns):
2313
2329
  # used instead of data_format.
2314
2330
  # The columns are left as optional for short term backwards compatibility.
2315
2331
  file_name = Column(String, nullable=True)
2316
- data_format = Column(String, nullable=True)
2332
+ data_format = Column(CITEXT, nullable=True)
2317
2333
 
2318
2334
  file_groups = relationship(
2319
2335
  "FileGroups",
@@ -3261,11 +3277,11 @@ class PreprocessedFiles(CommonColumns):
3261
3277
  ),
3262
3278
  )
3263
3279
 
3264
- file_name = Column(String)
3265
- object_url = Column(String)
3280
+ file_name = Column(String, nullable=False)
3281
+ object_url = Column(String, nullable=False)
3266
3282
  job_id = Column(Integer)
3267
- file_category = Column(String)
3268
- uploader_email = Column(String)
3283
+ file_category = Column(String, nullable=False)
3284
+ uploader_email = Column(String, nullable=False)
3269
3285
  status = Column(String)
3270
3286
  version = Column(Integer)
3271
3287
  released_version = Column(String)
@@ -3399,6 +3415,15 @@ class PreprocessedFiles(CommonColumns):
3399
3415
 
3400
3416
  return query.filter(cls.job_id.is_(None))
3401
3417
 
3418
+ @with_default_session
3419
+ def category_description(self, session: Session):
3420
+ category = (
3421
+ session.query(JobFileCategories)
3422
+ .filter(JobFileCategories.job_id == self.job_id, JobFileCategories.category == self.file_category)
3423
+ .first()
3424
+ )
3425
+ return category.description if category else None
3426
+
3402
3427
 
3403
3428
  INGESTION_JOB_STATUSES = [
3404
3429
  "DRAFT",
@@ -3433,7 +3458,7 @@ class IngestionJobs(CommonColumns):
3433
3458
  ),
3434
3459
  )
3435
3460
 
3436
- status = Column("status", Enum(*INGESTION_JOB_STATUSES, name="status"), nullable=False)
3461
+ status = Column("status", Enum(*INGESTION_JOB_STATUSES, name="ingestion_job_status"), nullable=False)
3437
3462
  trial_id = Column(String, nullable=False)
3438
3463
  version = Column(Integer, nullable=False)
3439
3464
  pending = Column(Boolean, nullable=False, default=False)
@@ -3522,16 +3547,18 @@ class JobFileCategories(CommonColumns):
3522
3547
  ["ingestion_jobs.id"],
3523
3548
  ),
3524
3549
  Index(
3525
- "idx_categories_job_id" "job_id",
3550
+ "idx_categories_job_id",
3551
+ "job_id",
3526
3552
  "category",
3527
3553
  unique=True,
3528
3554
  ),
3529
3555
  )
3530
3556
 
3531
3557
  category = Column(String)
3532
- job_id = Column(Integer)
3533
- type = Column(Enum("required", "optional", name="type"))
3534
- is_custom = Column(Boolean, default=False, server_default="false")
3558
+ description = Column(String)
3559
+ job_id = Column(Integer, nullable=False)
3560
+ type = Column(Enum("required", "optional", name="type"), nullable=False)
3561
+ is_custom = Column(Boolean, nullable=False, default=False, server_default="false")
3535
3562
 
3536
3563
  @staticmethod
3537
3564
  @with_default_session
@@ -3539,6 +3566,7 @@ class JobFileCategories(CommonColumns):
3539
3566
  category: str,
3540
3567
  job_id: int,
3541
3568
  type: str,
3569
+ description: str = None,
3542
3570
  is_custom: bool = False,
3543
3571
  session: Session = None,
3544
3572
  ):
@@ -3546,6 +3574,7 @@ class JobFileCategories(CommonColumns):
3546
3574
  category=category,
3547
3575
  job_id=job_id,
3548
3576
  type=type,
3577
+ description=description,
3549
3578
  is_custom=is_custom,
3550
3579
  )
3551
3580
  new_category.insert(session=session)
@@ -3557,6 +3586,11 @@ class JobFileCategories(CommonColumns):
3557
3586
  categories = session.query(cls).filter(cls.job_id == job_id, cls.type == type).all()
3558
3587
  return [c.category for c in categories]
3559
3588
 
3589
+ @classmethod
3590
+ @with_default_session
3591
+ def full_categories_for_job(cls, job_id: int, session: Session = None):
3592
+ return session.query(cls).filter_by(job_id=job_id).all()
3593
+
3560
3594
 
3561
3595
  class CategoryDataElements(CommonColumns):
3562
3596
  __tablename__ = "category_data_elements"
@@ -3567,7 +3601,8 @@ class CategoryDataElements(CommonColumns):
3567
3601
  ondelete="CASCADE",
3568
3602
  ),
3569
3603
  Index(
3570
- "idx_elements_category_id" "category_id",
3604
+ "idx_elements_category_id",
3605
+ "category_id",
3571
3606
  "name",
3572
3607
  unique=True,
3573
3608
  ),
@@ -3579,6 +3614,22 @@ class CategoryDataElements(CommonColumns):
3579
3614
  element_type = Column(String, nullable=False)
3580
3615
  cardinality = Column(String, nullable=True)
3581
3616
 
3617
+ @classmethod
3618
+ @with_default_session
3619
+ def elements_for_category(cls, category_id: int, session: Session = None):
3620
+ return session.query(cls).filter_by(category_id=category_id).all()
3621
+
3622
+ @classmethod
3623
+ @with_default_session
3624
+ def elements_by_category_for_job(cls, job_id: int, session: Session = None):
3625
+ """Fetch all CategoryDataElements for a job, along with the category name."""
3626
+ return (
3627
+ session.query(JobFileCategories.category, cls)
3628
+ .join(cls, cls.category_id == JobFileCategories.id)
3629
+ .filter(JobFileCategories.job_id == job_id)
3630
+ .all()
3631
+ )
3632
+
3582
3633
 
3583
3634
  class FileValidationErrors(CommonColumns):
3584
3635
  __tablename__ = "file_validation_errors"
@@ -0,0 +1,78 @@
1
+ from .additional_treatment import AdditionalTreatment
2
+ from .administrative_person import AdministrativePerson
3
+ from .administrative_role_assignment import AdministrativeRoleAssignment
4
+ from .adverse_event import AdverseEvent
5
+ from .arm import Arm
6
+ from .baseline_clinical_assessment import BaselineClinicalAssessment
7
+ from .cohort import Cohort
8
+ from .comorbidity import Comorbidity
9
+ from .consent_group import ConsentGroup
10
+ from .contact import Contact
11
+ from .demographic import Demographic
12
+ from .disease import Disease
13
+ from .exposure import Exposure
14
+ from .file import File
15
+ from .gvhd_diagnosis_acute import GVHDDiagnosisAcute
16
+ from .gvhd_diagnosis_chronic import GVHDDiagnosisChronic
17
+ from .gvhd_organ_acute import GVHDOrganAcute
18
+ from .gvhd_organ_chronic import GVHDOrganChronic
19
+ from .institution import Institution
20
+ from .medical_history import MedicalHistory
21
+ from .other_clinical_endpoint import OtherClinicalEndpoint
22
+ from .other_malignancy import OtherMalignancy
23
+ from .participant import Participant
24
+ from .prior_treatment import PriorTreatment
25
+ from .publication import Publication
26
+ from .radiotherapy_dose import RadiotherapyDose
27
+ from .response import Response
28
+ from .response_by_system import ResponseBySystem
29
+ from .shipment import Shipment
30
+ from .shipment_specimen import ShipmentSpecimen
31
+ from .specimen import Specimen
32
+ from .stem_cell_transplant import StemCellTransplant
33
+ from .surgery import Surgery
34
+ from .therapy_agent_dose import TherapyAgentDose
35
+ from .treatment import Treatment
36
+ from .trial import Trial
37
+
38
+
39
+ __all__ = [
40
+ "AdditionalTreatment",
41
+ "AdministrativePerson",
42
+ "AdministrativeRoleAssignment",
43
+ "AdverseEvent",
44
+ "Arm",
45
+ "BaselineClinicalAssessment",
46
+ "Cohort",
47
+ "Comorbidity",
48
+ "ConsentGroup",
49
+ "Contact",
50
+ "Demographic",
51
+ "Disease",
52
+ "Exposure",
53
+ "File",
54
+ "GVHDDiagnosisAcute",
55
+ "GVHDOrganAcute",
56
+ "GVHDDiagnosisChronic",
57
+ "GVHDOrganChronic",
58
+ "Institution",
59
+ "MedicalHistory",
60
+ "OtherClinicalEndpoint",
61
+ "OtherMalignancy",
62
+ "Participant",
63
+ "PriorTreatment",
64
+ "Publication",
65
+ "RadiotherapyDose",
66
+ "Response",
67
+ "ResponseBySystem",
68
+ "Shipment",
69
+ "ShipmentSpecimen",
70
+ "Specimen",
71
+ "StemCellTransplant",
72
+ "Surgery",
73
+ "TherapyAgentDose",
74
+ "Treatment",
75
+ "Trial",
76
+ ]
77
+
78
+ all_models = [globals()[cls_name] for cls_name in __all__]
@@ -0,0 +1,23 @@
1
+ from pydantic import NonNegativeInt
2
+ from .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: int | None = None
14
+
15
+ # Number of days from the enrollment date to the first recorded administration or occurrence of the treatment modality.
16
+ 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
+ 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
+ description: str
@@ -0,0 +1,30 @@
1
+ from .base import Base
2
+
3
+
4
+ class AdministrativePerson(Base):
5
+
6
+ # The unique internal identifier for the administrative person
7
+ administrative_person_id: int | None = None
8
+
9
+ # The internal identifier for the Institution the administrative person belongs to
10
+ institution_id: int | None = None
11
+
12
+ # The word or group of words indicating a person's first (personal or given) name, e.g. "John"
13
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2179589%20and%20ver_nr=2
14
+ first_name: str
15
+
16
+ # The word or group of worlds indicating a person's middle name, e.g. "Alan"
17
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2179590%20and%20ver_nr=2
18
+ middle_name: str | None = None
19
+
20
+ # The means of identifying an individual by using a word or group of words indicating a person's last (family) name, e.g. "Smith"
21
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2179591%20and%20ver_nr=2
22
+ last_name: str
23
+
24
+ # The string of characters that represents the electronic mail address of a person.
25
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2517550%20and%20ver_nr=1
26
+ email: str | None = None
27
+
28
+ # The string of digits that represent a telephone number that can be used to contact the person.
29
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2179593%20and%20ver_nr=3
30
+ phone_number: str | None = None
@@ -0,0 +1,16 @@
1
+ from .base import Base
2
+ from cidc_api.models.types import AdministrativeRole
3
+
4
+
5
+ class AdministrativeRoleAssignment(Base):
6
+ # The unique identifier for the associated trial
7
+ trial_id: int | None = None
8
+
9
+ # The version number of the trial dataset
10
+ version: str | None = None
11
+
12
+ # The unique identifier for the associated administrative person
13
+ administrative_person_id: int
14
+
15
+ # The role the administrative_person is performing for the associated trial
16
+ administrative_role: AdministrativeRole
@@ -0,0 +1,100 @@
1
+ from typing import Self
2
+
3
+ from pydantic import NonNegativeInt, model_validator
4
+
5
+ from .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: int | 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,16 @@
1
+ from .base import Base
2
+
3
+
4
+ class Arm(Base):
5
+ # The unique internal identifier for the arm
6
+ arm_id: int | None = None
7
+
8
+ # The unique identifier for the associated trial
9
+ trial_id: int | None = None
10
+
11
+ # The version number of the trial dataset
12
+ version: str | None = None
13
+
14
+ # The name of the arm, e.g. "Arm A1"
15
+ # CDE: https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2001626%20and%20ver_nr=3
16
+ name: str