phactor 0.2.0__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.
@@ -0,0 +1,739 @@
1
+ """Pydantic models mirroring the Phactor GraphQL schema for cohort analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Helper config for all models: camelCase aliasing
11
+ # ---------------------------------------------------------------------------
12
+
13
+ def _alias_generator(field_name: str) -> str:
14
+ """Convert snake_case to camelCase."""
15
+ parts = field_name.split("_")
16
+ return parts[0] + "".join(w.capitalize() for w in parts[1:])
17
+
18
+
19
+ _MODEL_CONFIG = ConfigDict(
20
+ populate_by_name=True,
21
+ alias_generator=_alias_generator,
22
+ use_enum_values=True,
23
+ )
24
+
25
+
26
+ # ============================================================================
27
+ # ENUMS
28
+ # ============================================================================
29
+
30
+
31
+ class TimingOperator(str, Enum):
32
+ WITHIN = "WITHIN"
33
+ BEFORE = "BEFORE"
34
+ AFTER = "AFTER"
35
+ GREATER_THAN_OR_EQUALS = "GREATER_THAN_OR_EQUALS"
36
+ LESS_THAN_OR_EQUALS = "LESS_THAN_OR_EQUALS"
37
+ BETWEEN = "BETWEEN"
38
+
39
+
40
+ class TimingUnit(str, Enum):
41
+ DAYS = "DAYS"
42
+ WEEKS = "WEEKS"
43
+ MONTHS = "MONTHS"
44
+ YEARS = "YEARS"
45
+
46
+
47
+ class TimingReference(str, Enum):
48
+ SCREENING = "SCREENING"
49
+ RANDOMIZATION = "RANDOMIZATION"
50
+ ENROLLMENT = "ENROLLMENT"
51
+ BASELINE = "BASELINE"
52
+ INDEX_DATE = "INDEX_DATE"
53
+ NOW = "NOW"
54
+
55
+
56
+ class ComparisonOperatorEnum(str, Enum):
57
+ EQUALS = "EQUALS"
58
+ GREATER_THAN = "GREATER_THAN"
59
+ LESS_THAN = "LESS_THAN"
60
+ GREATER_THAN_OR_EQUALS = "GREATER_THAN_OR_EQUALS"
61
+ LESS_THAN_OR_EQUALS = "LESS_THAN_OR_EQUALS"
62
+ BETWEEN = "BETWEEN"
63
+
64
+
65
+ class GenderValue(str, Enum):
66
+ MALE = "MALE"
67
+ FEMALE = "FEMALE"
68
+ OTHER = "OTHER"
69
+
70
+
71
+ class RaceValue(str, Enum):
72
+ UNKNOWN = "UNKNOWN"
73
+ WHITE = "WHITE"
74
+ ASIAN = "ASIAN"
75
+ BLACK = "BLACK"
76
+ NATIVE_HAWAIIAN = "NATIVE_HAWAIIAN"
77
+ AMERICAN_INDIAN = "AMERICAN_INDIAN"
78
+
79
+
80
+ class LocationTypeEnum(str, Enum):
81
+ ZIP = "ZIP"
82
+ CITY = "CITY"
83
+ STATE = "STATE"
84
+ COORDS = "COORDS"
85
+
86
+
87
+ # Code systems mirror the Query API GraphQL schema (ConditionTypeEnum) one-to-one.
88
+ class ConditionTypeEnum(str, Enum):
89
+ DBCONDID = "DBCONDID" # DrugBank condition identifier
90
+ ICD10 = "ICD10"
91
+ SNOMED = "SNOMED"
92
+ ICD10CM = "ICD10CM"
93
+ ICD9 = "ICD9"
94
+ ICD11 = "ICD11"
95
+ MEDDRA = "MEDDRA"
96
+
97
+
98
+ # Clinical status qualifier — mirrors the Query API GraphQL schema
99
+ # (ConditionClinicalStatus) one-to-one.
100
+ class ConditionClinicalStatus(str, Enum):
101
+ ACTIVE = "ACTIVE"
102
+ RECURRENCE = "RECURRENCE"
103
+ RELAPSE = "RELAPSE"
104
+ INACTIVE = "INACTIVE"
105
+ REMISSION = "REMISSION"
106
+ RESOLVED = "RESOLVED"
107
+
108
+
109
+ # Severity qualifier — mirrors the Query API GraphQL schema
110
+ # (ConditionSeverity) one-to-one.
111
+ class ConditionSeverity(str, Enum):
112
+ MILD = "MILD"
113
+ MODERATE = "MODERATE"
114
+ SEVERE = "SEVERE"
115
+
116
+
117
+ # Code systems mirror the Query API GraphQL schema (MedicationTypeEnum) one-to-one.
118
+ class MedicationTypeEnum(str, Enum):
119
+ PCID = "PCID" # DrugBank product concept ID
120
+ RXCUI = "RXCUI"
121
+ NDC = "NDC"
122
+ ATC = "ATC"
123
+
124
+
125
+ # FHIR MedicationStatement.status filter — mirrors the Query API GraphQL
126
+ # schema (MedicationStatus) one-to-one.
127
+ class MedicationStatus(str, Enum):
128
+ ACTIVE = "ACTIVE"
129
+ COMPLETED = "COMPLETED"
130
+ ENTERED_IN_ERROR = "ENTERED_IN_ERROR"
131
+ INTENDED = "INTENDED"
132
+ STOPPED = "STOPPED"
133
+ ON_HOLD = "ON_HOLD"
134
+ UNKNOWN = "UNKNOWN"
135
+ NOT_TAKEN = "NOT_TAKEN"
136
+
137
+
138
+ # Code systems mirror the Query API GraphQL schema (ProcedureTypeEnum) one-to-one.
139
+ class ProcedureTypeEnum(str, Enum):
140
+ CPT = "CPT"
141
+ SNOMED = "SNOMED"
142
+ HCPCS = "HCPCS"
143
+ ICD10PCS = "ICD10PCS"
144
+
145
+
146
+ # FHIR Procedure.outcome filter — mirrors the Query API GraphQL schema
147
+ # (ProcedureOutcome) one-to-one.
148
+ class ProcedureOutcome(str, Enum):
149
+ SUCCESSFUL = "SUCCESSFUL"
150
+ PARTIALLY_SUCCESSFUL = "PARTIALLY_SUCCESSFUL"
151
+ UNSUCCESSFUL = "UNSUCCESSFUL"
152
+
153
+
154
+ # Code systems mirror the Query API GraphQL schema (AllergyTypeEnum) one-to-one.
155
+ class AllergyTypeEnum(str, Enum):
156
+ RXCUI = "RXCUI"
157
+ NDC = "NDC"
158
+ SNOMED = "SNOMED"
159
+
160
+
161
+ # FHIR AllergyIntolerance.criticality filter — mirrors the Query API GraphQL
162
+ # schema (AllergyCriticality) one-to-one.
163
+ class AllergyCriticality(str, Enum):
164
+ LOW = "LOW"
165
+ HIGH = "HIGH"
166
+ UNABLE_TO_ASSESS = "UNABLE_TO_ASSESS"
167
+
168
+
169
+ # Code systems mirror the Query API GraphQL schema (ImmunizationTypeEnum) one-to-one.
170
+ class ImmunizationTypeEnum(str, Enum):
171
+ CVX = "CVX"
172
+ NDC = "NDC"
173
+ SNOMED = "SNOMED"
174
+
175
+
176
+ # HL7 v3 Observation Interpretation codes (subset of Table 0078 that FHIR
177
+ # Observation.interpretation binds to) — mirrors the Query API GraphQL schema
178
+ # (InterpretationCode) one-to-one.
179
+ class InterpretationCode(str, Enum):
180
+ N = "N" # Normal
181
+ H = "H" # High
182
+ L = "L" # Low
183
+ HH = "HH" # Critically high (panic high)
184
+ LL = "LL" # Critically low (panic low)
185
+ HU = "HU" # Significantly above upper limit
186
+ LU = "LU" # Significantly below lower limit
187
+ A = "A" # Abnormal (use when no high/low direction applies)
188
+ AA = "AA" # Critically abnormal
189
+
190
+
191
+ class ClinicalMeasurementCategory(str, Enum):
192
+ EXAMINATION = "EXAMINATION"
193
+ FUNCTIONAL_TEST = "FUNCTIONAL_TEST"
194
+ CLINICAL_SCORE = "CLINICAL_SCORE"
195
+ BIOMARKER = "BIOMARKER"
196
+ VITAL_SIGN = "VITAL_SIGN"
197
+ BODY_MEASUREMENT = "BODY_MEASUREMENT"
198
+ SOCIAL_HISTORY = "SOCIAL_HISTORY"
199
+ DEVICE = "DEVICE"
200
+
201
+
202
+ class StabilityStatusEnum(str, Enum):
203
+ ACTIVE = "ACTIVE"
204
+ INTERRUPTED = "INTERRUPTED"
205
+ ENDED = "ENDED"
206
+
207
+
208
+ class CriteriaGroupOperator(str, Enum):
209
+ AND = "AND"
210
+ OR = "OR"
211
+ SOME = "SOME"
212
+
213
+
214
+ # Top-level operator combining multiple cohort_groups (mirrors the Query API
215
+ # GraphQL schema's CohortGroupOperator). Distinct from CriteriaGroupOperator,
216
+ # which combines criteria groups *within* a single cohort group.
217
+ class InclusionGroupOperator(str, Enum):
218
+ """Relationship between inclusion criteria groups inside one pathway."""
219
+
220
+ AND = "AND"
221
+ OR = "OR"
222
+
223
+
224
+ class CohortGroupOperator(str, Enum):
225
+ """Relationship between complete cohort pathways."""
226
+
227
+ AND = "AND"
228
+ OR = "OR"
229
+
230
+
231
+ class CriterionErrorReason(str, Enum):
232
+ NO_TRANSLATION = "NO_TRANSLATION"
233
+ NO_PATH = "NO_PATH"
234
+
235
+
236
+ class CriterionErrorDomain(str, Enum):
237
+ CONDITION = "CONDITION"
238
+ PROCEDURE = "PROCEDURE"
239
+ ALLERGY = "ALLERGY"
240
+ IMMUNIZATION = "IMMUNIZATION"
241
+ MEDICATION = "MEDICATION"
242
+
243
+
244
+ class ResultGrouping(str, Enum):
245
+ NONE = "NONE"
246
+ STATE = "STATE"
247
+ CITY = "CITY"
248
+ PROXIMITY = "PROXIMITY"
249
+
250
+
251
+ class ProximityMode(str, Enum):
252
+ RADIUS = "RADIUS"
253
+ DISTANCE_BANDS = "DISTANCE_BANDS"
254
+
255
+
256
+ # ============================================================================
257
+ # INPUT MODELS
258
+ # ============================================================================
259
+
260
+
261
+ class TimingInput(BaseModel):
262
+ model_config = _MODEL_CONFIG
263
+
264
+ operator: TimingOperator
265
+ value: int | None = None
266
+ value_to: int | None = None
267
+ unit: TimingUnit
268
+ reference: TimingReference
269
+ reference_end: TimingReference | None = None
270
+ reference_date: str | None = None
271
+
272
+
273
+ class AgeRangeInput(BaseModel):
274
+ model_config = _MODEL_CONFIG
275
+
276
+ from_: int | None = Field(None, alias="from")
277
+ to: int | None = None
278
+
279
+
280
+ class CoordsInput(BaseModel):
281
+ model_config = _MODEL_CONFIG
282
+
283
+ lat: float
284
+ lng: float
285
+
286
+
287
+ class LocationValueInput(BaseModel):
288
+ model_config = _MODEL_CONFIG
289
+
290
+ type: LocationTypeEnum
291
+ value: str
292
+ radius: int
293
+ coords: CoordsInput | None = None
294
+
295
+
296
+ class LocationInput(BaseModel):
297
+ model_config = _MODEL_CONFIG
298
+
299
+ values: list[LocationValueInput] | None = None
300
+
301
+
302
+ class ConditionStageInput(BaseModel):
303
+ # FHIR Condition.stage.type — staging vocabulary (e.g. TNM, FIGO,
304
+ # AnnArbor). `value` is meaningless without naming the `system`.
305
+ model_config = _MODEL_CONFIG
306
+
307
+ system: str
308
+ value: str
309
+
310
+
311
+ class ConditionInput(BaseModel):
312
+ model_config = _MODEL_CONFIG
313
+
314
+ type: ConditionTypeEnum
315
+ values: list[str]
316
+ timing: TimingInput | None = None
317
+ # Clinical qualifier filters (all optional). Supplying any narrows
318
+ # categorically, AND-combined with the base type/value/timing match.
319
+ status: list[ConditionClinicalStatus] | None = None
320
+ severity: list[ConditionSeverity] | None = None
321
+ stage: list[ConditionStageInput] | None = None
322
+
323
+ @model_validator(mode="before")
324
+ @classmethod
325
+ def _reject_renamed_value_field(cls, data: object) -> object:
326
+ # 0.1.x used a singular `value: str`. Without this check the stray
327
+ # key is silently ignored and the error surfaces as an opaque
328
+ # "values: Field required" — point at the rename instead.
329
+ if isinstance(data, dict) and "value" in data and "values" not in data:
330
+ raise ValueError(
331
+ "`value` was renamed to `values` (a list of strings) in 0.2.0 "
332
+ "— pass values=[...] instead. See CHANGELOG.md."
333
+ )
334
+ return data
335
+
336
+
337
+ class ConditionsInput(BaseModel):
338
+ model_config = _MODEL_CONFIG
339
+
340
+ values: list[ConditionInput] | None = None
341
+
342
+
343
+ class PdcFilterInput(BaseModel):
344
+ model_config = _MODEL_CONFIG
345
+
346
+ min_pdc: float
347
+ window_days: int | None = None
348
+
349
+
350
+ class StableDoseFilterInput(BaseModel):
351
+ model_config = _MODEL_CONFIG
352
+
353
+ min_daily_dose_mg: float | None = None
354
+ min_consecutive_days: int | None = None
355
+
356
+
357
+ class AdherenceCriteriaInput(BaseModel):
358
+ model_config = _MODEL_CONFIG
359
+
360
+ therapy_status: list[StabilityStatusEnum] | None = None
361
+ pdc: PdcFilterInput | None = None
362
+ stable_dose: StableDoseFilterInput | None = None
363
+ gap_threshold_days: int | None = None
364
+
365
+
366
+ class MedicationInput(BaseModel):
367
+ model_config = _MODEL_CONFIG
368
+
369
+ type: MedicationTypeEnum
370
+ values: list[str]
371
+ last_ordered: str | None = None
372
+ timing: TimingInput | None = None
373
+ duration: int | None = None
374
+ duration_unit: TimingUnit | None = None
375
+ adherence: AdherenceCriteriaInput | None = None
376
+ # FHIR MedicationStatement.status filter (optional). Connector codegen
377
+ # routes to bitmap_medications_by_status.
378
+ status: list[MedicationStatus] | None = None
379
+
380
+
381
+ class MedicationsInput(BaseModel):
382
+ model_config = _MODEL_CONFIG
383
+
384
+ values: list[MedicationInput] | None = None
385
+
386
+
387
+ class ProcedureInput(BaseModel):
388
+ model_config = _MODEL_CONFIG
389
+
390
+ type: ProcedureTypeEnum
391
+ values: list[str]
392
+ timing: TimingInput | None = None
393
+ # FHIR Procedure.outcome filter (optional) — clinical result of the
394
+ # procedure, distinct from its lifecycle status.
395
+ outcome: list[ProcedureOutcome] | None = None
396
+
397
+
398
+ class ProceduresInput(BaseModel):
399
+ model_config = _MODEL_CONFIG
400
+
401
+ # `criteriaErrors` is an internal forwarding field the connector
402
+ # populates; the SDK omits it, mirroring ConditionsInput.
403
+ values: list[ProcedureInput] | None = None
404
+
405
+
406
+ class AllergyInput(BaseModel):
407
+ model_config = _MODEL_CONFIG
408
+
409
+ type: AllergyTypeEnum
410
+ values: list[str]
411
+ timing: TimingInput | None = None
412
+ # FHIR AllergyIntolerance.criticality filter (optional) — patient risk
413
+ # assessment, distinct from the reaction severity column.
414
+ criticality: list[AllergyCriticality] | None = None
415
+
416
+
417
+ class AllergiesInput(BaseModel):
418
+ model_config = _MODEL_CONFIG
419
+
420
+ # `criteriaErrors` is an internal forwarding field the connector
421
+ # populates; the SDK omits it, mirroring ConditionsInput.
422
+ values: list[AllergyInput] | None = None
423
+
424
+
425
+ class ImmunizationInput(BaseModel):
426
+ model_config = _MODEL_CONFIG
427
+
428
+ type: ImmunizationTypeEnum
429
+ values: list[str]
430
+ timing: TimingInput | None = None
431
+
432
+
433
+ class ImmunizationsInput(BaseModel):
434
+ model_config = _MODEL_CONFIG
435
+
436
+ # `criteriaErrors` is an internal forwarding field the connector
437
+ # populates; the SDK omits it, mirroring ConditionsInput.
438
+ values: list[ImmunizationInput] | None = None
439
+
440
+
441
+ class LabValueInput(BaseModel):
442
+ model_config = _MODEL_CONFIG
443
+
444
+ test_code: str | None = None
445
+ test_name: str | None = None
446
+ # operator/value/unit are optional so a categorical-only query
447
+ # (interpretation + testCode, no numeric comparison) is legal.
448
+ operator: ComparisonOperatorEnum | None = None
449
+ value: float | None = None
450
+ value_to: float | None = None
451
+ unit: str | None = None
452
+ timing: TimingInput | None = None
453
+ # Categorical filter on the HL7 v3 interpretation flag.
454
+ interpretation: list[InterpretationCode] | None = None
455
+
456
+ @model_validator(mode="after")
457
+ def _require_numeric_or_interpretation(self) -> LabValueInput:
458
+ # The connector silently skips a lab criterion that carries neither a
459
+ # complete numeric comparison nor an interpretation, inflating counts;
460
+ # fail fast here instead. A lab value's numeric triple is operator +
461
+ # value + unit.
462
+ has_numeric = (
463
+ self.operator is not None and self.value is not None and self.unit is not None
464
+ )
465
+ if not has_numeric and not self.interpretation:
466
+ raise ValueError(
467
+ "LabValueInput requires either a complete numeric comparison "
468
+ "(operator AND value AND unit) or a non-empty interpretation."
469
+ )
470
+ if self.test_code is None and self.test_name is None:
471
+ raise ValueError(
472
+ "LabValueInput requires at least one of test_code or test_name."
473
+ )
474
+ return self
475
+
476
+
477
+ class ClinicalMeasurementInput(BaseModel):
478
+ model_config = _MODEL_CONFIG
479
+
480
+ category: ClinicalMeasurementCategory
481
+ code: str | None = None
482
+ name: str | None = None
483
+ # operator/value are optional so a categorical-only query
484
+ # (interpretation + code) is legal.
485
+ operator: ComparisonOperatorEnum | None = None
486
+ value: float | None = None
487
+ value_to: float | None = None
488
+ unit: str | None = None
489
+ timing: TimingInput | None = None
490
+ # Categorical filter on the HL7 v3 interpretation flag.
491
+ interpretation: list[InterpretationCode] | None = None
492
+
493
+ @model_validator(mode="after")
494
+ def _require_numeric_or_interpretation(self) -> ClinicalMeasurementInput:
495
+ # Mirrors LabValueInput, but the clinical-measurement numeric pair is
496
+ # operator + value only — unit is NOT part of the pair (connector
497
+ # asymmetry, base-query-builder.ts `_clinicalMeasurementsWhereBody`).
498
+ # Same silent-skip hazard.
499
+ has_numeric = self.operator is not None and self.value is not None
500
+ if not has_numeric and not self.interpretation:
501
+ raise ValueError(
502
+ "ClinicalMeasurementInput requires either a numeric comparison "
503
+ "(operator AND value) or a non-empty interpretation."
504
+ )
505
+ return self
506
+
507
+
508
+ class PropositionInput(BaseModel):
509
+ model_config = _MODEL_CONFIG
510
+
511
+ age: AgeRangeInput | None = None
512
+ gender: list[GenderValue] | None = None
513
+ race: list[RaceValue] | None = None
514
+ medications: MedicationsInput | None = None
515
+ location: LocationInput | None = None
516
+ conditions: ConditionsInput | None = None
517
+ procedures: ProceduresInput | None = None
518
+ allergies: AllergiesInput | None = None
519
+ immunizations: ImmunizationsInput | None = None
520
+ lab_values: list[LabValueInput] | None = None
521
+ clinical_measurements: list[ClinicalMeasurementInput] | None = None
522
+
523
+
524
+ class CriteriaGroupInput(BaseModel):
525
+ model_config = _MODEL_CONFIG
526
+
527
+ name: str | None = None
528
+ operator: CriteriaGroupOperator
529
+ some_threshold: int | None = None
530
+ propositions: list[PropositionInput]
531
+
532
+
533
+ class CohortGroupInput(BaseModel):
534
+ model_config = _MODEL_CONFIG
535
+
536
+ inclusion_group_operator: InclusionGroupOperator
537
+ inclusion_groups: list[CriteriaGroupInput] | None = None
538
+ exclusion_groups: list[CriteriaGroupInput] | None = None
539
+
540
+
541
+ class StudyContextInput(BaseModel):
542
+ model_config = _MODEL_CONFIG
543
+
544
+ nct_id: str | None = None
545
+ study_title: str | None = None
546
+ study_phase: str | None = None
547
+ therapeutic_area: str | None = None
548
+
549
+
550
+ class SiteInput(BaseModel):
551
+ model_config = _MODEL_CONFIG
552
+
553
+ name: str
554
+ latitude: float
555
+ longitude: float
556
+ radius_miles: int | None = None
557
+
558
+
559
+ class GroupingInput(BaseModel):
560
+ model_config = _MODEL_CONFIG
561
+
562
+ type: ResultGrouping
563
+ proximity_mode: ProximityMode | None = None
564
+ sites: list[SiteInput] | None = None
565
+ default_radius_miles: int | None = None
566
+ distance_bands_miles: list[int] | None = None
567
+ max_groups: int | None = None
568
+ # Opt-in exact radius / distance-band counts (PostGIS ST_DWithin
569
+ # refine over the approximate H3 hex cover). Default None (approximate).
570
+ exact: bool | None = None
571
+
572
+
573
+ class GlobalFiltersInput(BaseModel):
574
+ model_config = _MODEL_CONFIG
575
+
576
+ geography: LocationInput | None = None
577
+
578
+
579
+ class AnalyzeCohortsInput(BaseModel):
580
+ model_config = _MODEL_CONFIG
581
+
582
+ providers: list[str]
583
+ cohort_groups: list[CohortGroupInput]
584
+ cohort_group_operator: CohortGroupOperator | None = None
585
+ study_context: StudyContextInput | None = None
586
+ include_impact_analysis: bool | None = None
587
+ include_demographic_breakdown: bool | None = None
588
+ include_combined_demographic_breakdown: bool | None = None
589
+ grouping: GroupingInput | None = None
590
+ global_filters: GlobalFiltersInput | None = None
591
+
592
+
593
+ # ============================================================================
594
+ # RESPONSE MODELS
595
+ # ============================================================================
596
+
597
+
598
+ class ProviderMeta(BaseModel):
599
+ model_config = _MODEL_CONFIG
600
+
601
+ id: str
602
+ name: str
603
+
604
+
605
+ class AnalysisWarning(BaseModel):
606
+ model_config = _MODEL_CONFIG
607
+
608
+ provider: ProviderMeta
609
+ code: str
610
+ message: str
611
+
612
+
613
+ class CriterionImpact(BaseModel):
614
+ model_config = _MODEL_CONFIG
615
+
616
+ proposition_index: int
617
+ proposition_label: str
618
+ matching_count: int
619
+ excluded_count: int
620
+ impact_percentage: float
621
+
622
+
623
+ class BucketCount(BaseModel):
624
+ model_config = _MODEL_CONFIG
625
+
626
+ range_from: int
627
+ range_to: int
628
+ count: int
629
+ percentage: float
630
+
631
+
632
+ class LabeledCount(BaseModel):
633
+ model_config = _MODEL_CONFIG
634
+
635
+ label: str
636
+ count: int
637
+ percentage: float
638
+
639
+
640
+ class DemographicBreakdown(BaseModel):
641
+ model_config = _MODEL_CONFIG
642
+
643
+ age_distribution: list[BucketCount] | None = None
644
+ gender_distribution: list[LabeledCount] | None = None
645
+ race_distribution: list[LabeledCount] | None = None
646
+ geographic_distribution: list[LabeledCount] | None = None
647
+
648
+
649
+ class CriterionError(BaseModel):
650
+ model_config = _MODEL_CONFIG
651
+
652
+ domain: CriterionErrorDomain
653
+ source_type: str
654
+ source_value: str
655
+ reason: CriterionErrorReason
656
+ message: str | None = None
657
+
658
+
659
+ class CriteriaGroupResult(BaseModel):
660
+ model_config = _MODEL_CONFIG
661
+
662
+ name: str
663
+ total_patients_in_group: int | None
664
+ query: str
665
+ evaluated: bool
666
+ criteria_errors: list[CriterionError]
667
+ criterion_impacts: list[CriterionImpact] | None = None
668
+ demographic_breakdown: DemographicBreakdown | None = None
669
+
670
+
671
+ class CohortGroupResult(BaseModel):
672
+ model_config = _MODEL_CONFIG
673
+
674
+ total_patients_in_group: int | None
675
+ query: str
676
+ evaluated: bool
677
+ criteria_errors: list[CriterionError]
678
+ inclusion_groups: list[CriteriaGroupResult]
679
+ exclusion_groups: list[CriteriaGroupResult]
680
+ demographic_breakdown: DemographicBreakdown | None = None
681
+
682
+
683
+ class GeographicGroup(BaseModel):
684
+ model_config = _MODEL_CONFIG
685
+
686
+ label: str
687
+ count: int
688
+ percentage: float
689
+
690
+
691
+ class SiteRadiusResult(BaseModel):
692
+ model_config = _MODEL_CONFIG
693
+
694
+ site_name: str
695
+ latitude: float
696
+ longitude: float
697
+ radius_miles: int
698
+ count: int
699
+ percentage: float
700
+
701
+
702
+ class DistanceBand(BaseModel):
703
+ model_config = _MODEL_CONFIG
704
+
705
+ label: str
706
+ min_miles: int
707
+ max_miles: int | None = None
708
+ count: int
709
+ percentage: float
710
+
711
+
712
+ class SiteDistanceBandResult(BaseModel):
713
+ model_config = _MODEL_CONFIG
714
+
715
+ site_name: str
716
+ latitude: float
717
+ longitude: float
718
+ bands: list[DistanceBand]
719
+
720
+
721
+ class ProviderCohortAnalysis(BaseModel):
722
+ model_config = _MODEL_CONFIG
723
+
724
+ id: str
725
+ provider: ProviderMeta
726
+ total_patients: int | None
727
+ cohort_groups: list[CohortGroupResult]
728
+ combined_demographic_breakdown: DemographicBreakdown | None = None
729
+ geographic_groups: list[GeographicGroup] | None = None
730
+ site_radius_results: list[SiteRadiusResult] | None = None
731
+ site_distance_band_results: list[SiteDistanceBandResult] | None = None
732
+
733
+
734
+ class AnalyzeCohortsPayload(BaseModel):
735
+ model_config = _MODEL_CONFIG
736
+
737
+ id: str
738
+ analyses: list[ProviderCohortAnalysis]
739
+ warnings: list[AnalysisWarning] | None = None