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
phactor/cohorts/query.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""GraphQL query constant for analyzeCohorts."""
|
|
2
|
+
|
|
3
|
+
ANALYZE_COHORTS_QUERY = """
|
|
4
|
+
query AnalyzeCohorts($input: AnalyzeCohortsInput!) {
|
|
5
|
+
analyzeCohorts(input: $input) {
|
|
6
|
+
id
|
|
7
|
+
analyses {
|
|
8
|
+
id
|
|
9
|
+
provider {
|
|
10
|
+
id
|
|
11
|
+
name
|
|
12
|
+
}
|
|
13
|
+
totalPatients
|
|
14
|
+
cohortGroups {
|
|
15
|
+
totalPatientsInGroup
|
|
16
|
+
query
|
|
17
|
+
evaluated
|
|
18
|
+
criteriaErrors { domain sourceType sourceValue reason message }
|
|
19
|
+
inclusionGroups {
|
|
20
|
+
name
|
|
21
|
+
totalPatientsInGroup
|
|
22
|
+
query
|
|
23
|
+
evaluated
|
|
24
|
+
criteriaErrors { domain sourceType sourceValue reason message }
|
|
25
|
+
criterionImpacts {
|
|
26
|
+
propositionIndex propositionLabel matchingCount excludedCount impactPercentage
|
|
27
|
+
}
|
|
28
|
+
demographicBreakdown {
|
|
29
|
+
ageDistribution { rangeFrom rangeTo count percentage }
|
|
30
|
+
genderDistribution { label count percentage }
|
|
31
|
+
raceDistribution { label count percentage }
|
|
32
|
+
geographicDistribution { label count percentage }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exclusionGroups {
|
|
36
|
+
name
|
|
37
|
+
totalPatientsInGroup
|
|
38
|
+
query
|
|
39
|
+
evaluated
|
|
40
|
+
criteriaErrors { domain sourceType sourceValue reason message }
|
|
41
|
+
criterionImpacts {
|
|
42
|
+
propositionIndex propositionLabel matchingCount excludedCount impactPercentage
|
|
43
|
+
}
|
|
44
|
+
demographicBreakdown {
|
|
45
|
+
ageDistribution { rangeFrom rangeTo count percentage }
|
|
46
|
+
genderDistribution { label count percentage }
|
|
47
|
+
raceDistribution { label count percentage }
|
|
48
|
+
geographicDistribution { label count percentage }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
demographicBreakdown {
|
|
52
|
+
ageDistribution {
|
|
53
|
+
rangeFrom
|
|
54
|
+
rangeTo
|
|
55
|
+
count
|
|
56
|
+
percentage
|
|
57
|
+
}
|
|
58
|
+
genderDistribution {
|
|
59
|
+
label
|
|
60
|
+
count
|
|
61
|
+
percentage
|
|
62
|
+
}
|
|
63
|
+
raceDistribution {
|
|
64
|
+
label
|
|
65
|
+
count
|
|
66
|
+
percentage
|
|
67
|
+
}
|
|
68
|
+
geographicDistribution {
|
|
69
|
+
label
|
|
70
|
+
count
|
|
71
|
+
percentage
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
combinedDemographicBreakdown {
|
|
76
|
+
ageDistribution { rangeFrom rangeTo count percentage }
|
|
77
|
+
genderDistribution { label count percentage }
|
|
78
|
+
raceDistribution { label count percentage }
|
|
79
|
+
geographicDistribution { label count percentage }
|
|
80
|
+
}
|
|
81
|
+
geographicGroups { label count percentage }
|
|
82
|
+
siteRadiusResults { siteName latitude longitude radiusMiles count percentage }
|
|
83
|
+
siteDistanceBandResults {
|
|
84
|
+
siteName latitude longitude
|
|
85
|
+
bands { label minMiles maxMiles count percentage }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
warnings {
|
|
89
|
+
provider {
|
|
90
|
+
id
|
|
91
|
+
name
|
|
92
|
+
}
|
|
93
|
+
code
|
|
94
|
+
message
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
"""
|
phactor/config.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Configuration for the Phactor SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
# Client-side request ceiling. Deliberately above the server's own budget
|
|
10
|
+
# (connector statement_timeout 420s < query-api fan-out 540s < ALB idle 600s) so
|
|
11
|
+
# the server always wins the race: a slow cohort comes back as a GraphQL error
|
|
12
|
+
# naming the provider, rather than a bare client TimeoutError that is then
|
|
13
|
+
# retried three times and triples the load.
|
|
14
|
+
DEFAULT_TIMEOUT_SECONDS = 600.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class PhactorConfig:
|
|
19
|
+
"""Immutable SDK configuration with environment variable fallback.
|
|
20
|
+
|
|
21
|
+
Environment variables:
|
|
22
|
+
PHACTOR_CLIENT_ID
|
|
23
|
+
PHACTOR_CLIENT_SECRET
|
|
24
|
+
PHACTOR_ENTITY_ID
|
|
25
|
+
PHACTOR_FUSIONAUTH_URL
|
|
26
|
+
PHACTOR_GATEWAY_URL
|
|
27
|
+
PHACTOR_CA_BUNDLE
|
|
28
|
+
PHACTOR_TIMEOUT
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
client_id: str
|
|
32
|
+
client_secret: str
|
|
33
|
+
entity_id: str | None
|
|
34
|
+
fusionauth_url: str
|
|
35
|
+
gateway_url: str
|
|
36
|
+
verify: str | bool
|
|
37
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_kwargs(
|
|
41
|
+
cls,
|
|
42
|
+
*,
|
|
43
|
+
client_id: str | None = None,
|
|
44
|
+
client_secret: str | None = None,
|
|
45
|
+
entity_id: str | None = None, # Optional: omit for plain client_credentials auth
|
|
46
|
+
fusionauth_url: str | None = None,
|
|
47
|
+
gateway_url: str | None = None,
|
|
48
|
+
verify: str | bool | None = None,
|
|
49
|
+
timeout: float | None = None,
|
|
50
|
+
) -> PhactorConfig:
|
|
51
|
+
"""Create config from explicit values with env var fallback."""
|
|
52
|
+
|
|
53
|
+
def _resolve(value: str | None, env_key: str, name: str) -> str:
|
|
54
|
+
result = value or os.environ.get(env_key)
|
|
55
|
+
if not result:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"{name} must be provided or set via {env_key} environment variable"
|
|
58
|
+
)
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
def _resolve_timeout(value: float | None) -> float:
|
|
62
|
+
if value is not None:
|
|
63
|
+
return value
|
|
64
|
+
raw = os.environ.get("PHACTOR_TIMEOUT")
|
|
65
|
+
if not raw:
|
|
66
|
+
return DEFAULT_TIMEOUT_SECONDS
|
|
67
|
+
try:
|
|
68
|
+
return float(raw)
|
|
69
|
+
except ValueError as exc:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"PHACTOR_TIMEOUT must be a number of seconds, got {raw!r}"
|
|
72
|
+
) from exc
|
|
73
|
+
|
|
74
|
+
def _resolve_verify(value: str | bool | None) -> str | bool:
|
|
75
|
+
if value is False:
|
|
76
|
+
warnings.warn(
|
|
77
|
+
"TLS certificate verification is disabled. "
|
|
78
|
+
"This should only be used for local development.",
|
|
79
|
+
stacklevel=3,
|
|
80
|
+
)
|
|
81
|
+
if value is not None:
|
|
82
|
+
return value
|
|
83
|
+
return os.environ.get("PHACTOR_CA_BUNDLE") or True
|
|
84
|
+
|
|
85
|
+
return cls(
|
|
86
|
+
client_id=_resolve(client_id, "PHACTOR_CLIENT_ID", "client_id"),
|
|
87
|
+
client_secret=_resolve(client_secret, "PHACTOR_CLIENT_SECRET", "client_secret"),
|
|
88
|
+
entity_id=entity_id or os.environ.get("PHACTOR_ENTITY_ID"),
|
|
89
|
+
fusionauth_url=_resolve(
|
|
90
|
+
fusionauth_url, "PHACTOR_FUSIONAUTH_URL", "fusionauth_url"
|
|
91
|
+
).rstrip("/"),
|
|
92
|
+
gateway_url=_resolve(gateway_url, "PHACTOR_GATEWAY_URL", "gateway_url"),
|
|
93
|
+
verify=_resolve_verify(verify),
|
|
94
|
+
timeout=_resolve_timeout(timeout),
|
|
95
|
+
)
|
phactor/exceptions.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Exception hierarchy for the Phactor SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PhactorError(Exception):
|
|
7
|
+
"""Base exception for all Phactor SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, *, status_code: int | None = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthenticationError(PhactorError):
|
|
15
|
+
"""Raised when authentication fails (invalid credentials, expired token, 401)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GraphQLError(PhactorError):
|
|
19
|
+
"""Raised when the GraphQL API returns errors in the response body."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, *, errors: list[dict[str, object]] | None = None) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.errors = errors or []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ValidationError(PhactorError):
|
|
27
|
+
"""Raised when input validation fails (e.g., builder misuse)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TimeoutError(PhactorError):
|
|
31
|
+
"""Raised when an HTTP request times out."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConnectionError(PhactorError):
|
|
35
|
+
"""Raised when the SDK cannot reach the server."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NotFoundError(PhactorError):
|
|
39
|
+
"""Raised on HTTP 404 responses."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RateLimitError(PhactorError):
|
|
43
|
+
"""Raised on HTTP 429 responses after retry exhaustion."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ServerError(PhactorError):
|
|
47
|
+
"""Raised on HTTP 5xx responses after retry exhaustion."""
|