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.
- phactor/__init__.py +159 -0
- phactor/_http.py +183 -0
- phactor/_version.py +1 -0
- phactor/auth.py +167 -0
- phactor/client.py +172 -0
- phactor/cohorts/__init__.py +143 -0
- phactor/cohorts/builder.py +156 -0
- phactor/cohorts/client.py +283 -0
- phactor/cohorts/models.py +739 -0
- phactor/cohorts/query.py +98 -0
- phactor/config.py +95 -0
- phactor/exceptions.py +47 -0
- phactor/pandas.py +589 -0
- phactor/py.typed +0 -0
- phactor-0.2.0.dist-info/METADATA +237 -0
- phactor-0.2.0.dist-info/RECORD +18 -0
- phactor-0.2.0.dist-info/WHEEL +4 -0
- phactor-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -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
|