phactor 0.3.1__tar.gz → 0.4.0__tar.gz

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 (26) hide show
  1. {phactor-0.3.1 → phactor-0.4.0}/CHANGELOG.md +44 -0
  2. {phactor-0.3.1 → phactor-0.4.0}/PKG-INFO +55 -1
  3. {phactor-0.3.1 → phactor-0.4.0}/README.md +54 -0
  4. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/__init__.py +12 -0
  5. phactor-0.4.0/src/phactor/_version.py +1 -0
  6. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/cohorts/client.py +5 -1
  7. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/cohorts/models.py +87 -4
  8. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/cohorts/query.py +8 -0
  9. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/sites/models.py +3 -2
  10. phactor-0.3.1/src/phactor/_version.py +0 -1
  11. {phactor-0.3.1 → phactor-0.4.0}/.gitignore +0 -0
  12. {phactor-0.3.1 → phactor-0.4.0}/LICENSE +0 -0
  13. {phactor-0.3.1 → phactor-0.4.0}/pyproject.toml +0 -0
  14. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/_graphql.py +0 -0
  15. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/_http.py +0 -0
  16. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/auth.py +0 -0
  17. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/client.py +0 -0
  18. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/cohorts/__init__.py +0 -0
  19. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/cohorts/builder.py +0 -0
  20. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/config.py +0 -0
  21. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/exceptions.py +0 -0
  22. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/pandas.py +0 -0
  23. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/py.typed +0 -0
  24. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/sites/__init__.py +0 -0
  25. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/sites/client.py +0 -0
  26. {phactor-0.3.1 → phactor-0.4.0}/src/phactor/sites/query.py +0 -0
@@ -6,6 +6,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  While the SDK is pre-1.0 (`0.x`), breaking changes may accompany a minor version bump.
8
8
 
9
+ ## [0.4.0] - 2026-10-06
10
+
11
+ ### Added
12
+
13
+ - **Nearby provider sites.** A `PROXIMITY` grouping can set
14
+ `"includeNearbyProviderSites": True` (`GroupingInput.include_nearby_provider_sites`)
15
+ to also get your subscribed providers' sites inside each radius or distance
16
+ band. The result is `AnalyzeCohortsPayload.nearby_provider_sites`, `None`
17
+ when not requested.
18
+ - New models `NearbyProviderSites` (`status`, `truncated`, `sites`),
19
+ `NearbyProviderSite` (address, coordinates, `providers`, `matches`),
20
+ `NearbyProviderRef` (`id`, `name`, `provider_type`, `color`) and
21
+ `NearbyProviderSiteMatch` (`site_name`, coordinates, `distance_miles`).
22
+ - New enums `NearbyProviderSitesStatus` (`AVAILABLE`, `UNAVAILABLE`) and
23
+ `DataProviderType`. Like the 0.3.2 response enums, an unknown value from
24
+ the API is kept as a plain string.
25
+ - At most 500 sites are returned, nearest first; `truncated` is `True` when
26
+ more matched.
27
+ - With the option on, the server rejects the request (`GraphQLError`) when it
28
+ has more than 50 distinct providers, more than 25 grouping sites, or a radius
29
+ or distance band outside 1-250 miles. Without it these limits do not apply.
30
+
31
+ ## [0.3.2] - 2026-09-24
32
+
33
+ ### Fixed
34
+
35
+ - `client.cohorts.analyze` no longer fails when a provider reports a criterion
36
+ error reason other than `NO_TRANSLATION` / `NO_PATH`. 0.3.1 raised a pydantic
37
+ `ValidationError` for `UNSUPPORTED_SEMANTICS` (returned today, for example,
38
+ for exact product codes a provider cannot match), so one unevaluated criterion
39
+ made the whole result unreadable.
40
+
41
+ ### Added
42
+
43
+ - `CriterionErrorReason.UNSUPPORTED_SEMANTICS`, `INSUFFICIENT_DATA`,
44
+ `MATERIALIZATION_NOT_READY` and `CriterionErrorDomain.AGE`, `MEASUREMENT`,
45
+ `ENCOUNTER`, matching the API.
46
+
47
+ ### Changed
48
+
49
+ - Response values this SDK version does not know yet (`CriterionError.reason`,
50
+ `CriterionError.domain`, `Site.access`) are kept as plain strings instead of
51
+ failing validation, so a value the API adds later cannot break parsing.
52
+
9
53
  ## [0.3.1] - 2026-09-24
10
54
 
11
55
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: phactor
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for the Phactor clinical trial feasibility platform
5
5
  Project-URL: Homepage, https://phactor.ai
6
6
  Project-URL: Documentation, https://docs.phactor.ai
@@ -282,3 +282,57 @@ There are three independent operator levels:
282
282
  Exclusion groups are always unioned and subtracted from the pathway's combined
283
283
  inclusion result. For example, an `OR` pathway with inclusion groups `A` and `B`
284
284
  and exclusion group `E` evaluates as `(A OR B) EXCEPT E`.
285
+
286
+ ## Geographic Grouping
287
+
288
+ Group cohort results by geography using PROXIMITY and RADIUS or DISTANCE_BANDS mode:
289
+
290
+ ```python
291
+ result = client.cohorts.analyze(
292
+ providers=[...],
293
+ cohort_groups=[...],
294
+ grouping={
295
+ "type": "PROXIMITY",
296
+ "proximityMode": "RADIUS",
297
+ "sites": [
298
+ {"name": "Johns Hopkins", "latitude": 39.2963, "longitude": -76.5927, "radiusMiles": 25}
299
+ ],
300
+ },
301
+ )
302
+ for analysis in result.analyses:
303
+ for group in analysis.site_radius_results or []:
304
+ print(group.site_name, group.count, group.percentage)
305
+ ```
306
+
307
+ ### Provider sites near each grouping site
308
+
309
+ ```python
310
+ from phactor import NearbyProviderSitesStatus
311
+
312
+ result = client.cohorts.analyze(
313
+ providers=[...],
314
+ cohort_groups=[...],
315
+ grouping={
316
+ "type": "PROXIMITY",
317
+ "proximityMode": "RADIUS",
318
+ "sites": [
319
+ {"name": "Johns Hopkins", "latitude": 39.2963, "longitude": -76.5927, "radiusMiles": 25}
320
+ ],
321
+ "includeNearbyProviderSites": True,
322
+ },
323
+ )
324
+ nearby = result.nearby_provider_sites
325
+ if nearby and nearby.status == NearbyProviderSitesStatus.AVAILABLE:
326
+ for site in nearby.sites:
327
+ print(site.name, [p.name for p in site.providers], [m.distance_miles for m in site.matches])
328
+ ```
329
+
330
+ Only sites of subscribed providers you included in `providers` are returned. Each provider
331
+ carries its `provider_type` (a `DataProviderType`) and its display `color` (`#rrggbb`).
332
+ `NearbyProviderSitesStatus.UNAVAILABLE` means the site lookup failed; the cohort counts are
333
+ still valid. At most the nearest 500 sites are returned; `truncated` is `True` when there were
334
+ more.
335
+
336
+ With `includeNearbyProviderSites` on, the request is rejected with a `GraphQLError` when it has
337
+ more than 50 distinct providers, more than 25 grouping sites, or a radius or distance band outside
338
+ 1-250 miles.
@@ -243,3 +243,57 @@ There are three independent operator levels:
243
243
  Exclusion groups are always unioned and subtracted from the pathway's combined
244
244
  inclusion result. For example, an `OR` pathway with inclusion groups `A` and `B`
245
245
  and exclusion group `E` evaluates as `(A OR B) EXCEPT E`.
246
+
247
+ ## Geographic Grouping
248
+
249
+ Group cohort results by geography using PROXIMITY and RADIUS or DISTANCE_BANDS mode:
250
+
251
+ ```python
252
+ result = client.cohorts.analyze(
253
+ providers=[...],
254
+ cohort_groups=[...],
255
+ grouping={
256
+ "type": "PROXIMITY",
257
+ "proximityMode": "RADIUS",
258
+ "sites": [
259
+ {"name": "Johns Hopkins", "latitude": 39.2963, "longitude": -76.5927, "radiusMiles": 25}
260
+ ],
261
+ },
262
+ )
263
+ for analysis in result.analyses:
264
+ for group in analysis.site_radius_results or []:
265
+ print(group.site_name, group.count, group.percentage)
266
+ ```
267
+
268
+ ### Provider sites near each grouping site
269
+
270
+ ```python
271
+ from phactor import NearbyProviderSitesStatus
272
+
273
+ result = client.cohorts.analyze(
274
+ providers=[...],
275
+ cohort_groups=[...],
276
+ grouping={
277
+ "type": "PROXIMITY",
278
+ "proximityMode": "RADIUS",
279
+ "sites": [
280
+ {"name": "Johns Hopkins", "latitude": 39.2963, "longitude": -76.5927, "radiusMiles": 25}
281
+ ],
282
+ "includeNearbyProviderSites": True,
283
+ },
284
+ )
285
+ nearby = result.nearby_provider_sites
286
+ if nearby and nearby.status == NearbyProviderSitesStatus.AVAILABLE:
287
+ for site in nearby.sites:
288
+ print(site.name, [p.name for p in site.providers], [m.distance_miles for m in site.matches])
289
+ ```
290
+
291
+ Only sites of subscribed providers you included in `providers` are returned. Each provider
292
+ carries its `provider_type` (a `DataProviderType`) and its display `color` (`#rrggbb`).
293
+ `NearbyProviderSitesStatus.UNAVAILABLE` means the site lookup failed; the cohort counts are
294
+ still valid. At most the nearest 500 sites are returned; `truncated` is `True` when there were
295
+ more.
296
+
297
+ With `includeNearbyProviderSites` on, the request is rejected with a `GraphQLError` when it has
298
+ more than 50 distinct providers, more than 25 grouping sites, or a radius or distance band outside
299
+ 1-250 miles.
@@ -30,6 +30,7 @@ from phactor.cohorts.models import (
30
30
  CriterionError,
31
31
  CriterionErrorDomain,
32
32
  CriterionErrorReason,
33
+ DataProviderType,
33
34
  DistanceBand,
34
35
  GenderValue,
35
36
  GeographicGroup,
@@ -48,6 +49,11 @@ from phactor.cohorts.models import (
48
49
  MedicationsInput,
49
50
  MedicationStatus,
50
51
  MedicationTypeEnum,
52
+ NearbyProviderRef,
53
+ NearbyProviderSite,
54
+ NearbyProviderSiteMatch,
55
+ NearbyProviderSites,
56
+ NearbyProviderSitesStatus,
51
57
  PdcFilterInput,
52
58
  ProcedureInput,
53
59
  ProcedureOutcome,
@@ -113,6 +119,8 @@ __all__ = [
113
119
  "CriterionErrorReason",
114
120
  "ResultGrouping",
115
121
  "ProximityMode",
122
+ "NearbyProviderSitesStatus",
123
+ "DataProviderType",
116
124
  # Input models
117
125
  "TimingInput",
118
126
  "AgeRangeInput",
@@ -148,6 +156,10 @@ __all__ = [
148
156
  "CriterionError",
149
157
  "DistanceBand",
150
158
  "GeographicGroup",
159
+ "NearbyProviderRef",
160
+ "NearbyProviderSite",
161
+ "NearbyProviderSiteMatch",
162
+ "NearbyProviderSites",
151
163
  "SiteDistanceBandResult",
152
164
  "SiteRadiusResult",
153
165
  # Site search
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -156,7 +156,11 @@ class CohortsClient:
156
156
  age_buckets: Custom age range buckets for demographic breakdown.
157
157
  include_combined_demographic_breakdown:
158
158
  Include combined-population demographic breakdown.
159
- grouping: Geographic grouping configuration dict.
159
+ grouping: Geographic grouping configuration dict. With
160
+ ``includeNearbyProviderSites`` on, the server rejects the request
161
+ (``GraphQLError``) when it has more than 50 distinct providers,
162
+ more than 25 grouping sites, or a radius or distance band outside
163
+ 1-250 miles.
160
164
  global_filters: Optional global filters applied to all cohort groups
161
165
  (e.g. restrict the study to specific states or geographies).
162
166
  Returns:
@@ -233,11 +233,23 @@ class CohortGroupOperator(str, Enum):
233
233
 
234
234
 
235
235
  class CriterionErrorReason(str, Enum):
236
- NO_TRANSLATION = "NO_TRANSLATION"
237
- NO_PATH = "NO_PATH"
236
+ """Why a provider could not evaluate a criterion. Values the server adds
237
+ later reach ``CriterionError.reason`` as plain strings instead of failing."""
238
+
239
+ UNSUPPORTED_SEMANTICS = "UNSUPPORTED_SEMANTICS"
240
+ INSUFFICIENT_DATA = "INSUFFICIENT_DATA"
241
+ MATERIALIZATION_NOT_READY = "MATERIALIZATION_NOT_READY"
242
+ NO_TRANSLATION = "NO_TRANSLATION" # a crosswalk route exists but this code has no entry
243
+ NO_PATH = "NO_PATH" # no translation route from the source system to the provider's
238
244
 
239
245
 
240
246
  class CriterionErrorDomain(str, Enum):
247
+ """The criterion type a ``CriterionError`` is about. Values the server adds
248
+ later reach ``CriterionError.domain`` as plain strings instead of failing."""
249
+
250
+ AGE = "AGE"
251
+ MEASUREMENT = "MEASUREMENT"
252
+ ENCOUNTER = "ENCOUNTER"
241
253
  CONDITION = "CONDITION"
242
254
  PROCEDURE = "PROCEDURE"
243
255
  ALLERGY = "ALLERGY"
@@ -596,6 +608,9 @@ class GroupingInput(BaseModel):
596
608
  # Opt-in exact radius / distance-band counts (PostGIS ST_DWithin
597
609
  # refine over the approximate H3 hex cover). Default None (approximate).
598
610
  exact: bool | None = None
611
+ # Opt-in: also return subscribed providers' sites inside each grouping radius
612
+ # (AnalyzeCohortsPayload.nearby_provider_sites). PROXIMITY only.
613
+ include_nearby_provider_sites: bool | None = None
599
614
 
600
615
 
601
616
  class GlobalFiltersInput(BaseModel):
@@ -677,10 +692,12 @@ class DemographicBreakdown(BaseModel):
677
692
  class CriterionError(BaseModel):
678
693
  model_config = _MODEL_CONFIG
679
694
 
680
- domain: CriterionErrorDomain
695
+ # Response enums fall back to the raw string for values this SDK version
696
+ # does not know yet, so a new server value never breaks parsing a result.
697
+ domain: CriterionErrorDomain | str = Field(union_mode="left_to_right")
681
698
  source_type: str
682
699
  source_value: str
683
- reason: CriterionErrorReason
700
+ reason: CriterionErrorReason | str = Field(union_mode="left_to_right")
684
701
  message: str | None = None
685
702
 
686
703
 
@@ -759,9 +776,75 @@ class ProviderCohortAnalysis(BaseModel):
759
776
  site_distance_band_results: list[SiteDistanceBandResult] | None = None
760
777
 
761
778
 
779
+ class NearbyProviderSitesStatus(str, Enum):
780
+ """Whether the nearby provider site lookup succeeded. Values the server adds
781
+ later reach ``NearbyProviderSites.status`` as plain strings instead of failing."""
782
+
783
+ AVAILABLE = "AVAILABLE"
784
+ UNAVAILABLE = "UNAVAILABLE"
785
+
786
+
787
+ class DataProviderType(str, Enum):
788
+ """The kind of data a provider holds. Values the server adds later reach
789
+ ``NearbyProviderRef.provider_type`` as plain strings instead of failing."""
790
+
791
+ PHARMACY = "PHARMACY"
792
+ HOSPITAL = "HOSPITAL"
793
+ LABORATORY = "LABORATORY"
794
+ EHR_SYSTEM = "EHR_SYSTEM"
795
+ CLAIMS = "CLAIMS"
796
+ GENOMICS = "GENOMICS"
797
+ OTHER = "OTHER"
798
+
799
+
800
+ class NearbyProviderRef(BaseModel):
801
+ model_config = _MODEL_CONFIG
802
+
803
+ id: str
804
+ name: str
805
+ # A value this SDK version does not know yet stays a plain string.
806
+ provider_type: DataProviderType | str = Field(union_mode="left_to_right")
807
+ # The provider's display color as a ``#rrggbb`` hex string.
808
+ color: str
809
+
810
+
811
+ class NearbyProviderSiteMatch(BaseModel):
812
+ model_config = _MODEL_CONFIG
813
+
814
+ site_name: str
815
+ latitude: float
816
+ longitude: float
817
+ distance_miles: float
818
+
819
+
820
+ class NearbyProviderSite(BaseModel):
821
+ model_config = _MODEL_CONFIG
822
+
823
+ id: str
824
+ name: str
825
+ address: str | None = None
826
+ city: str | None = None
827
+ state: str | None = None
828
+ postal_code: str | None = None
829
+ latitude: float
830
+ longitude: float
831
+ providers: list[NearbyProviderRef]
832
+ matches: list[NearbyProviderSiteMatch]
833
+
834
+
835
+ class NearbyProviderSites(BaseModel):
836
+ model_config = _MODEL_CONFIG
837
+
838
+ # A value this SDK version does not know yet stays a plain string.
839
+ status: NearbyProviderSitesStatus | str = Field(union_mode="left_to_right")
840
+ truncated: bool
841
+ sites: list[NearbyProviderSite]
842
+
843
+
762
844
  class AnalyzeCohortsPayload(BaseModel):
763
845
  model_config = _MODEL_CONFIG
764
846
 
765
847
  id: str
766
848
  analyses: list[ProviderCohortAnalysis]
767
849
  warnings: list[AnalysisWarning] | None = None
850
+ nearby_provider_sites: NearbyProviderSites | None = None
@@ -93,6 +93,14 @@ query AnalyzeCohorts($input: AnalyzeCohortsInput!) {
93
93
  code
94
94
  message
95
95
  }
96
+ nearbyProviderSites {
97
+ status truncated
98
+ sites {
99
+ id name address city state postalCode latitude longitude
100
+ providers { id name providerType color }
101
+ matches { siteName latitude longitude distanceMiles }
102
+ }
103
+ }
96
104
  }
97
105
  }
98
106
  """
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from enum import Enum
6
6
 
7
- from pydantic import BaseModel, ConfigDict
7
+ from pydantic import BaseModel, ConfigDict, Field
8
8
  from pydantic.alias_generators import to_camel
9
9
 
10
10
  from phactor.cohorts.models import LocationTypeEnum, LocationValueInput
@@ -34,7 +34,8 @@ class Site(BaseModel):
34
34
  country: str | None = None
35
35
  latitude: float
36
36
  longitude: float
37
- access: SiteAccess
37
+ # A value this SDK version does not know yet stays a plain string.
38
+ access: SiteAccess | str = Field(union_mode="left_to_right")
38
39
 
39
40
  def as_location(self, *, radius: int = 10) -> LocationValueInput:
40
41
  """A SITE location value matching patients within ``radius`` miles of this site.
@@ -1 +0,0 @@
1
- __version__ = "0.3.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes