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 ADDED
@@ -0,0 +1,159 @@
1
+ """Phactor Python SDK — programmatic access to the Phactor clinical trial feasibility platform."""
2
+
3
+ from phactor._version import __version__
4
+ from phactor.client import AsyncPhactorClient, PhactorClient
5
+ from phactor.cohorts.builder import CohortBuilder, Proposition
6
+ from phactor.cohorts.models import (
7
+ AdherenceCriteriaInput,
8
+ AgeRangeInput,
9
+ AllergiesInput,
10
+ AllergyCriticality,
11
+ AllergyInput,
12
+ AllergyTypeEnum,
13
+ AnalyzeCohortsInput,
14
+ AnalyzeCohortsPayload,
15
+ ClinicalMeasurementCategory,
16
+ ClinicalMeasurementInput,
17
+ CohortGroupInput,
18
+ CohortGroupOperator,
19
+ ComparisonOperatorEnum,
20
+ ConditionClinicalStatus,
21
+ ConditionInput,
22
+ ConditionSeverity,
23
+ ConditionsInput,
24
+ ConditionStageInput,
25
+ ConditionTypeEnum,
26
+ CoordsInput,
27
+ CriteriaGroupInput,
28
+ CriteriaGroupOperator,
29
+ CriteriaGroupResult,
30
+ CriterionError,
31
+ CriterionErrorDomain,
32
+ CriterionErrorReason,
33
+ DistanceBand,
34
+ GenderValue,
35
+ GeographicGroup,
36
+ GlobalFiltersInput,
37
+ GroupingInput,
38
+ ImmunizationInput,
39
+ ImmunizationsInput,
40
+ ImmunizationTypeEnum,
41
+ InclusionGroupOperator,
42
+ InterpretationCode,
43
+ LabValueInput,
44
+ LocationInput,
45
+ LocationTypeEnum,
46
+ LocationValueInput,
47
+ MedicationInput,
48
+ MedicationsInput,
49
+ MedicationStatus,
50
+ MedicationTypeEnum,
51
+ PdcFilterInput,
52
+ ProcedureInput,
53
+ ProcedureOutcome,
54
+ ProceduresInput,
55
+ ProcedureTypeEnum,
56
+ PropositionInput,
57
+ ProximityMode,
58
+ RaceValue,
59
+ ResultGrouping,
60
+ SiteDistanceBandResult,
61
+ SiteInput,
62
+ SiteRadiusResult,
63
+ StabilityStatusEnum,
64
+ StableDoseFilterInput,
65
+ TimingInput,
66
+ TimingOperator,
67
+ TimingReference,
68
+ TimingUnit,
69
+ )
70
+ from phactor.config import PhactorConfig
71
+ from phactor.exceptions import (
72
+ AuthenticationError,
73
+ ConnectionError,
74
+ GraphQLError,
75
+ PhactorError,
76
+ TimeoutError,
77
+ ValidationError,
78
+ )
79
+
80
+ __all__ = [
81
+ "__version__",
82
+ "AsyncPhactorClient",
83
+ "PhactorClient",
84
+ "PhactorConfig",
85
+ "CohortBuilder",
86
+ "Proposition",
87
+ # Enums
88
+ "TimingOperator",
89
+ "TimingUnit",
90
+ "TimingReference",
91
+ "ComparisonOperatorEnum",
92
+ "GenderValue",
93
+ "RaceValue",
94
+ "LocationTypeEnum",
95
+ "ConditionTypeEnum",
96
+ "ConditionClinicalStatus",
97
+ "ConditionSeverity",
98
+ "MedicationTypeEnum",
99
+ "MedicationStatus",
100
+ "ProcedureTypeEnum",
101
+ "ProcedureOutcome",
102
+ "AllergyTypeEnum",
103
+ "AllergyCriticality",
104
+ "ImmunizationTypeEnum",
105
+ "InterpretationCode",
106
+ "ClinicalMeasurementCategory",
107
+ "StabilityStatusEnum",
108
+ "CriteriaGroupOperator",
109
+ "InclusionGroupOperator",
110
+ "CohortGroupOperator",
111
+ "CriterionErrorDomain",
112
+ "CriterionErrorReason",
113
+ "ResultGrouping",
114
+ "ProximityMode",
115
+ # Input models
116
+ "TimingInput",
117
+ "AgeRangeInput",
118
+ "CoordsInput",
119
+ "LocationValueInput",
120
+ "LocationInput",
121
+ "ConditionStageInput",
122
+ "ConditionInput",
123
+ "ConditionsInput",
124
+ "PdcFilterInput",
125
+ "StableDoseFilterInput",
126
+ "AdherenceCriteriaInput",
127
+ "MedicationInput",
128
+ "MedicationsInput",
129
+ "ProcedureInput",
130
+ "ProceduresInput",
131
+ "AllergyInput",
132
+ "AllergiesInput",
133
+ "ImmunizationInput",
134
+ "ImmunizationsInput",
135
+ "LabValueInput",
136
+ "ClinicalMeasurementInput",
137
+ "PropositionInput",
138
+ "CriteriaGroupInput",
139
+ "CohortGroupInput",
140
+ "SiteInput",
141
+ "GroupingInput",
142
+ "GlobalFiltersInput",
143
+ "AnalyzeCohortsInput",
144
+ # Response models
145
+ "AnalyzeCohortsPayload",
146
+ "CriteriaGroupResult",
147
+ "CriterionError",
148
+ "DistanceBand",
149
+ "GeographicGroup",
150
+ "SiteDistanceBandResult",
151
+ "SiteRadiusResult",
152
+ # Exceptions
153
+ "AuthenticationError",
154
+ "ConnectionError",
155
+ "GraphQLError",
156
+ "PhactorError",
157
+ "TimeoutError",
158
+ "ValidationError",
159
+ ]
phactor/_http.py ADDED
@@ -0,0 +1,183 @@
1
+ """HTTP client wrappers with retry, auth injection, and error mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import random
7
+ import time
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from phactor.auth import AsyncTokenManager, TokenManager
13
+ from phactor.exceptions import (
14
+ AuthenticationError,
15
+ ConnectionError,
16
+ NotFoundError,
17
+ PhactorError,
18
+ RateLimitError,
19
+ ServerError,
20
+ TimeoutError,
21
+ )
22
+
23
+ _RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
24
+ _MAX_RETRIES = 3
25
+ _BASE_DELAY = 0.5
26
+ _MAX_JITTER = 0.25
27
+
28
+
29
+ def _map_status_error(response: httpx.Response) -> PhactorError:
30
+ """Map an HTTP error status to the appropriate SDK exception."""
31
+ status = response.status_code
32
+ body = response.text
33
+ if status == 401:
34
+ return AuthenticationError(f"Authentication failed: {body}", status_code=status)
35
+ if status == 403:
36
+ return AuthenticationError(f"Forbidden: {body}", status_code=status)
37
+ if status == 404:
38
+ return NotFoundError(f"Not found: {body}", status_code=status)
39
+ if status == 429:
40
+ return RateLimitError(f"Rate limited: {body}", status_code=status)
41
+ if status >= 500:
42
+ return ServerError(f"Server error ({status}): {body}", status_code=status)
43
+ return PhactorError(f"HTTP error ({status}): {body}", status_code=status)
44
+
45
+
46
+ def _retry_delay(response: httpx.Response | None, attempt: int) -> float:
47
+ delay: float = _BASE_DELAY * (2**attempt)
48
+ if response is not None:
49
+ retry_after = response.headers.get("Retry-After")
50
+ if retry_after:
51
+ with contextlib.suppress(ValueError):
52
+ delay = float(retry_after)
53
+ return delay + float(random.uniform(0.0, _MAX_JITTER))
54
+
55
+
56
+ class SyncHTTPClient:
57
+ """Synchronous HTTP client with auth injection and retry logic."""
58
+
59
+ def __init__(
60
+ self,
61
+ token_manager: TokenManager,
62
+ *,
63
+ timeout: float = 30.0,
64
+ verify: str | bool = True,
65
+ ) -> None:
66
+ self._token_manager = token_manager
67
+ self._client = httpx.Client(timeout=timeout, verify=verify)
68
+
69
+ def post(self, url: str, *, json: dict[str, Any]) -> dict[str, Any]:
70
+ """POST JSON with auth headers, retry on transient errors."""
71
+ attempt = 0
72
+ refreshed_token = False
73
+
74
+ while True:
75
+ try:
76
+ headers = self._token_manager.get_auth_headers(self._client)
77
+ response = self._client.post(url, json=json, headers=headers)
78
+
79
+ if response.status_code == 200:
80
+ return response.json() # type: ignore[no-any-return]
81
+
82
+ if response.status_code == 401:
83
+ if not refreshed_token:
84
+ self._token_manager.invalidate()
85
+ refreshed_token = True
86
+ attempt = 0
87
+ continue
88
+ raise _map_status_error(response)
89
+
90
+ if response.status_code in _RETRYABLE_STATUS_CODES and attempt < _MAX_RETRIES - 1:
91
+ time.sleep(_retry_delay(response, attempt))
92
+ attempt += 1
93
+ continue
94
+
95
+ raise _map_status_error(response)
96
+
97
+ except httpx.TimeoutException as exc:
98
+ if attempt < _MAX_RETRIES - 1:
99
+ time.sleep(_retry_delay(None, attempt))
100
+ attempt += 1
101
+ continue
102
+ raise TimeoutError(f"Request timed out: {exc}") from exc
103
+ except httpx.ConnectError as exc:
104
+ if attempt < _MAX_RETRIES - 1:
105
+ time.sleep(_retry_delay(None, attempt))
106
+ attempt += 1
107
+ continue
108
+ raise ConnectionError(f"Connection failed: {exc}") from exc
109
+ except httpx.RequestError as exc:
110
+ if attempt < _MAX_RETRIES - 1:
111
+ time.sleep(_retry_delay(None, attempt))
112
+ attempt += 1
113
+ continue
114
+ raise ConnectionError(f"Connection failed: {exc}") from exc
115
+
116
+ def close(self) -> None:
117
+ self._client.close()
118
+
119
+
120
+ class AsyncHTTPClient:
121
+ """Async HTTP client with auth injection and retry logic."""
122
+
123
+ def __init__(
124
+ self,
125
+ token_manager: AsyncTokenManager,
126
+ *,
127
+ timeout: float = 30.0,
128
+ verify: str | bool = True,
129
+ ) -> None:
130
+ self._token_manager = token_manager
131
+ self._client = httpx.AsyncClient(timeout=timeout, verify=verify)
132
+
133
+ async def post(self, url: str, *, json: dict[str, Any]) -> dict[str, Any]:
134
+ """POST JSON with auth headers, retry on transient errors."""
135
+ import asyncio
136
+
137
+ attempt = 0
138
+ refreshed_token = False
139
+
140
+ while True:
141
+ try:
142
+ headers = await self._token_manager.get_auth_headers(self._client)
143
+ response = await self._client.post(url, json=json, headers=headers)
144
+
145
+ if response.status_code == 200:
146
+ return response.json() # type: ignore[no-any-return]
147
+
148
+ if response.status_code == 401:
149
+ if not refreshed_token:
150
+ await self._token_manager.invalidate()
151
+ refreshed_token = True
152
+ attempt = 0
153
+ continue
154
+ raise _map_status_error(response)
155
+
156
+ if response.status_code in _RETRYABLE_STATUS_CODES and attempt < _MAX_RETRIES - 1:
157
+ await asyncio.sleep(_retry_delay(response, attempt))
158
+ attempt += 1
159
+ continue
160
+
161
+ raise _map_status_error(response)
162
+
163
+ except httpx.TimeoutException as exc:
164
+ if attempt < _MAX_RETRIES - 1:
165
+ await asyncio.sleep(_retry_delay(None, attempt))
166
+ attempt += 1
167
+ continue
168
+ raise TimeoutError(f"Request timed out: {exc}") from exc
169
+ except httpx.ConnectError as exc:
170
+ if attempt < _MAX_RETRIES - 1:
171
+ await asyncio.sleep(_retry_delay(None, attempt))
172
+ attempt += 1
173
+ continue
174
+ raise ConnectionError(f"Connection failed: {exc}") from exc
175
+ except httpx.RequestError as exc:
176
+ if attempt < _MAX_RETRIES - 1:
177
+ await asyncio.sleep(_retry_delay(None, attempt))
178
+ attempt += 1
179
+ continue
180
+ raise ConnectionError(f"Connection failed: {exc}") from exc
181
+
182
+ async def close(self) -> None:
183
+ await self._client.aclose()
phactor/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
phactor/auth.py ADDED
@@ -0,0 +1,167 @@
1
+ """Token management for FusionAuth client_credentials flow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import threading
7
+ import time
8
+ from dataclasses import dataclass
9
+
10
+ import httpx
11
+ import jwt
12
+
13
+ from phactor.config import PhactorConfig
14
+ from phactor.exceptions import AuthenticationError
15
+
16
+ # Match gateway's TOKEN_CACHE_TTL_MS = 55_000
17
+ _TOKEN_CACHE_TTL_S = 55
18
+
19
+
20
+ def _cache_ttl(expires_in: float) -> float:
21
+ return max(1.0, min(float(_TOKEN_CACHE_TTL_S), expires_in - 5.0))
22
+
23
+
24
+ @dataclass
25
+ class _CachedToken:
26
+ access_token: str
27
+ tenant_id: str
28
+ expires_at: float
29
+
30
+
31
+ class TokenManager:
32
+ """Acquires and caches OAuth2 client_credentials tokens from FusionAuth."""
33
+
34
+ def __init__(self, config: PhactorConfig) -> None:
35
+ self._config = config
36
+ self._token_url = f"{config.fusionauth_url}/oauth2/token"
37
+ self._scope = (
38
+ f"target-entity:{config.entity_id}:internal:read"
39
+ if config.entity_id
40
+ else None
41
+ )
42
+ self._cached: _CachedToken | None = None
43
+ self._lock = threading.Lock()
44
+
45
+ def _is_valid(self) -> bool:
46
+ return self._cached is not None and time.monotonic() < self._cached.expires_at
47
+
48
+ def _request_token(self, client: httpx.Client) -> _CachedToken:
49
+ data: dict[str, str] = {
50
+ "grant_type": "client_credentials",
51
+ "client_id": self._config.client_id,
52
+ "client_secret": self._config.client_secret,
53
+ }
54
+ if self._scope:
55
+ data["scope"] = self._scope
56
+ response = client.post(
57
+ self._token_url,
58
+ data=data,
59
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
60
+ )
61
+ if response.status_code != 200:
62
+ raise AuthenticationError(
63
+ f"Token request failed ({response.status_code}): {response.text}",
64
+ status_code=response.status_code,
65
+ )
66
+ data = response.json()
67
+ access_token = data["access_token"]
68
+ expires_in = float(data.get("expires_in", _TOKEN_CACHE_TTL_S + 5))
69
+
70
+ # Decode JWT without verification to extract tid (tenant ID)
71
+ decoded = jwt.decode(access_token, options={"verify_signature": False})
72
+ tenant_id = decoded.get("tid")
73
+ if not tenant_id:
74
+ raise AuthenticationError("Service token missing tid (tenant ID) claim")
75
+
76
+ ttl = _cache_ttl(expires_in)
77
+ return _CachedToken(
78
+ access_token=access_token,
79
+ tenant_id=tenant_id,
80
+ expires_at=time.monotonic() + ttl,
81
+ )
82
+
83
+ def get_auth_headers(self, client: httpx.Client) -> dict[str, str]:
84
+ """Return Authorization and x-tenant-id headers, refreshing if needed."""
85
+ with self._lock:
86
+ if not self._is_valid():
87
+ self._cached = self._request_token(client)
88
+ token = self._cached
89
+ assert token is not None
90
+ return {
91
+ "Authorization": f"Bearer {token.access_token}",
92
+ "x-tenant-id": token.tenant_id,
93
+ }
94
+
95
+ def invalidate(self) -> None:
96
+ """Force token refresh on next call."""
97
+ with self._lock:
98
+ self._cached = None
99
+
100
+
101
+ class AsyncTokenManager:
102
+ """Async variant of TokenManager."""
103
+
104
+ def __init__(self, config: PhactorConfig) -> None:
105
+ self._config = config
106
+ self._token_url = f"{config.fusionauth_url}/oauth2/token"
107
+ self._scope = (
108
+ f"target-entity:{config.entity_id}:internal:read"
109
+ if config.entity_id
110
+ else None
111
+ )
112
+ self._cached: _CachedToken | None = None
113
+ self._lock = asyncio.Lock()
114
+
115
+ def _is_valid(self) -> bool:
116
+ return self._cached is not None and time.monotonic() < self._cached.expires_at
117
+
118
+ async def _request_token(self, client: httpx.AsyncClient) -> _CachedToken:
119
+ data: dict[str, str] = {
120
+ "grant_type": "client_credentials",
121
+ "client_id": self._config.client_id,
122
+ "client_secret": self._config.client_secret,
123
+ }
124
+ if self._scope:
125
+ data["scope"] = self._scope
126
+ response = await client.post(
127
+ self._token_url,
128
+ data=data,
129
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
130
+ )
131
+ if response.status_code != 200:
132
+ raise AuthenticationError(
133
+ f"Token request failed ({response.status_code}): {response.text}",
134
+ status_code=response.status_code,
135
+ )
136
+ data = response.json()
137
+ access_token = data["access_token"]
138
+ expires_in = float(data.get("expires_in", _TOKEN_CACHE_TTL_S + 5))
139
+
140
+ decoded = jwt.decode(access_token, options={"verify_signature": False})
141
+ tenant_id = decoded.get("tid")
142
+ if not tenant_id:
143
+ raise AuthenticationError("Service token missing tid (tenant ID) claim")
144
+
145
+ ttl = _cache_ttl(expires_in)
146
+ return _CachedToken(
147
+ access_token=access_token,
148
+ tenant_id=tenant_id,
149
+ expires_at=time.monotonic() + ttl,
150
+ )
151
+
152
+ async def get_auth_headers(self, client: httpx.AsyncClient) -> dict[str, str]:
153
+ """Return Authorization and x-tenant-id headers, refreshing if needed."""
154
+ async with self._lock:
155
+ if not self._is_valid():
156
+ self._cached = await self._request_token(client)
157
+ token = self._cached
158
+ assert token is not None
159
+ return {
160
+ "Authorization": f"Bearer {token.access_token}",
161
+ "x-tenant-id": token.tenant_id,
162
+ }
163
+
164
+ async def invalidate(self) -> None:
165
+ """Force token refresh on next call."""
166
+ async with self._lock:
167
+ self._cached = None
phactor/client.py ADDED
@@ -0,0 +1,172 @@
1
+ """Top-level Phactor SDK clients (sync and async)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import TracebackType
6
+ from typing import Any
7
+
8
+ from phactor._http import AsyncHTTPClient, SyncHTTPClient
9
+ from phactor.auth import AsyncTokenManager, TokenManager
10
+ from phactor.cohorts.client import AsyncCohortsClient, CohortsClient
11
+ from phactor.cohorts.models import (
12
+ AnalyzeCohortsPayload,
13
+ CohortGroupInput,
14
+ CohortGroupOperator,
15
+ GlobalFiltersInput,
16
+ StudyContextInput,
17
+ )
18
+ from phactor.config import PhactorConfig
19
+ from phactor.exceptions import PhactorError
20
+
21
+
22
+ class PhactorClient:
23
+ """Synchronous Phactor SDK client.
24
+
25
+ Usage:
26
+ client = PhactorClient(client_id="...", client_secret="...", ...)
27
+ result = client.cohorts.analyze(providers=[...], cohort_groups=[...])
28
+ client.close()
29
+
30
+ Or as a context manager:
31
+ with PhactorClient(...) as client:
32
+ result = client.cohorts.analyze(...)
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ client_id: str | None = None,
39
+ client_secret: str | None = None,
40
+ entity_id: str | None = None,
41
+ fusionauth_url: str | None = None,
42
+ gateway_url: str | None = None,
43
+ verify: str | bool | None = None,
44
+ timeout: float | None = None,
45
+ ) -> None:
46
+ self._config = PhactorConfig.from_kwargs(
47
+ client_id=client_id,
48
+ client_secret=client_secret,
49
+ entity_id=entity_id,
50
+ fusionauth_url=fusionauth_url,
51
+ gateway_url=gateway_url,
52
+ verify=verify,
53
+ timeout=timeout,
54
+ )
55
+ token_manager = TokenManager(self._config)
56
+ self._http = SyncHTTPClient(
57
+ token_manager, timeout=self._config.timeout, verify=self._config.verify
58
+ )
59
+ self.cohorts = CohortsClient(self._http, self._config.gateway_url)
60
+
61
+ def close(self) -> None:
62
+ """Close the underlying HTTP client."""
63
+ self._http.close()
64
+
65
+ def __enter__(self) -> PhactorClient:
66
+ return self
67
+
68
+ def __exit__(
69
+ self,
70
+ exc_type: type[BaseException] | None,
71
+ exc_val: BaseException | None,
72
+ exc_tb: TracebackType | None,
73
+ ) -> None:
74
+ self.close()
75
+
76
+
77
+ class AsyncPhactorClient:
78
+ """Async Phactor SDK client.
79
+
80
+ Usage:
81
+ async with AsyncPhactorClient(...) as client:
82
+ result = await client.cohorts.analyze(...)
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ *,
88
+ client_id: str | None = None,
89
+ client_secret: str | None = None,
90
+ entity_id: str | None = None,
91
+ fusionauth_url: str | None = None,
92
+ gateway_url: str | None = None,
93
+ verify: str | bool | None = None,
94
+ timeout: float | None = None,
95
+ ) -> None:
96
+ self._config = PhactorConfig.from_kwargs(
97
+ client_id=client_id,
98
+ client_secret=client_secret,
99
+ entity_id=entity_id,
100
+ fusionauth_url=fusionauth_url,
101
+ gateway_url=gateway_url,
102
+ verify=verify,
103
+ timeout=timeout,
104
+ )
105
+ self._closed = False
106
+ token_manager = AsyncTokenManager(self._config)
107
+ self._http = AsyncHTTPClient(
108
+ token_manager, timeout=self._config.timeout, verify=self._config.verify
109
+ )
110
+ self._cohorts = AsyncCohortsClient(self._http, self._config.gateway_url)
111
+ self.cohorts = _AsyncCohortsProxy(self)
112
+
113
+ async def close(self) -> None:
114
+ """Close the underlying HTTP client."""
115
+ if self._closed:
116
+ return
117
+ self._closed = True
118
+ await self._http.close()
119
+
120
+ def _ensure_open(self) -> None:
121
+ if self._closed:
122
+ raise PhactorError("AsyncPhactorClient is closed")
123
+
124
+ async def __aenter__(self) -> AsyncPhactorClient:
125
+ return self
126
+
127
+ async def __aexit__(
128
+ self,
129
+ exc_type: type[BaseException] | None,
130
+ exc_val: BaseException | None,
131
+ exc_tb: TracebackType | None,
132
+ ) -> None:
133
+ await self.close()
134
+
135
+
136
+ class _AsyncCohortsProxy:
137
+ """Guarded async cohorts facade that fails fast after client close."""
138
+
139
+ def __init__(self, owner: AsyncPhactorClient) -> None:
140
+ self._owner = owner
141
+
142
+ async def analyze(
143
+ self,
144
+ providers: list[str],
145
+ cohort_groups: list[CohortGroupInput | dict[str, Any]],
146
+ *,
147
+ cohort_group_operator: CohortGroupOperator | str | None = None,
148
+ study_context: StudyContextInput | dict[str, Any] | None = None,
149
+ include_impact_analysis: bool | None = None,
150
+ include_demographic_breakdown: bool | None = None,
151
+ age_buckets: list[dict[str, int]] | None = None,
152
+ include_combined_demographic_breakdown: bool | None = None,
153
+ grouping: dict[str, Any] | None = None,
154
+ global_filters: GlobalFiltersInput | dict[str, Any] | None = None,
155
+ ) -> AnalyzeCohortsPayload:
156
+ self._owner._ensure_open()
157
+ return await self._owner._cohorts.analyze(
158
+ providers,
159
+ cohort_groups,
160
+ cohort_group_operator=cohort_group_operator,
161
+ study_context=study_context,
162
+ include_impact_analysis=include_impact_analysis,
163
+ include_demographic_breakdown=include_demographic_breakdown,
164
+ age_buckets=age_buckets,
165
+ include_combined_demographic_breakdown=include_combined_demographic_breakdown,
166
+ grouping=grouping,
167
+ global_filters=global_filters,
168
+ )
169
+
170
+ def __getattr__(self, name: str) -> Any:
171
+ self._owner._ensure_open()
172
+ return getattr(self._owner._cohorts, name)