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,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."""