clinia-context-engine 0.8.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,28 @@
1
+ from . import resources as resources
2
+ from ._auth import (
3
+ AsyncClientCredentialsTokenProvider,
4
+ ClientCredentials,
5
+ ClientCredentialsTokenProvider,
6
+ )
7
+ from ._errors import APIConnectionError, APIStatusError, APITimeoutError, ContextEngineError
8
+ from ._http import AsyncHttpClient, AsyncTokenProvider, HttpClient, TokenProvider
9
+ from .client import AsyncContextEngineClient, ContextEngineClient
10
+ from .generated import models as models
11
+
12
+ __all__ = [
13
+ "APIConnectionError",
14
+ "APIStatusError",
15
+ "APITimeoutError",
16
+ "AsyncClientCredentialsTokenProvider",
17
+ "AsyncContextEngineClient",
18
+ "AsyncHttpClient",
19
+ "AsyncTokenProvider",
20
+ "ClientCredentials",
21
+ "ClientCredentialsTokenProvider",
22
+ "ContextEngineClient",
23
+ "ContextEngineError",
24
+ "HttpClient",
25
+ "TokenProvider",
26
+ "models",
27
+ "resources",
28
+ ]
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Any, Literal
8
+ from urllib.parse import urlsplit
9
+
10
+ import httpx
11
+
12
+ from ._errors import ContextEngineError
13
+
14
+ __all__ = [
15
+ "AsyncClientCredentialsTokenProvider",
16
+ "ClientCredentials",
17
+ "ClientCredentialsTokenProvider",
18
+ ]
19
+
20
+ # Clinia's authorization server, applied when a config names neither a
21
+ # `token_url` nor an `issuer` — so `ClientCredentials(client_id, client_secret)`
22
+ # is a complete config for a Clinia workspace.
23
+ #
24
+ # The resource indicator is only defaulted alongside the token URL: it is an
25
+ # RFC 8707 indicator naming Clinia's protected resource, and sending it to
26
+ # somebody else's authorization server would be wrong.
27
+ CLINIA_OAUTH_TOKEN_URL = "https://console.clinia.cloud/api/auth/oauth2/token"
28
+ CLINIA_OAUTH_RESOURCE = "https://console.clinia.cloud"
29
+
30
+ DEFAULT_EXPIRY_MARGIN = 60.0
31
+ # Fallback lifetime when the token endpoint omits `expires_in`.
32
+ DEFAULT_TTL = 300.0
33
+
34
+
35
+ @dataclass(frozen=True, kw_only=True)
36
+ class ClientCredentials:
37
+ """OAuth2 client-credentials configuration.
38
+
39
+ For a Clinia workspace, `client_id` + `client_secret` is enough — the token
40
+ endpoint and `resource` indicator default to Clinia's authorization server.
41
+ Naming an `issuer` instead locates a different authorization server by
42
+ OIDC/OAuth2 discovery; naming a `token_url` points at one explicitly.
43
+ """
44
+
45
+ client_id: str
46
+ client_secret: str
47
+ token_url: str | None = None
48
+ issuer: str | None = None
49
+ # Discovery transformation used when `issuer` is given: OIDC (default) or
50
+ # RFC 8414.
51
+ discovery_algorithm: Literal["oidc", "oauth2"] = "oidc"
52
+ # RFC 8707 resource indicator naming the protected resource the token is
53
+ # for.
54
+ resource: str | None = None
55
+ # `audience` parameter used by some IdPs (e.g. Auth0).
56
+ audience: str | None = None
57
+ # Space-delimited scopes.
58
+ scope: str | None = None
59
+ # How the credentials are presented to the token endpoint: `post` (the
60
+ # default, what Clinia's authorization server expects) sends them in the
61
+ # request body; `basic` sends them in the Authorization header
62
+ # (RFC 6749 §2.3.1).
63
+ auth_method: Literal["basic", "post"] = "post"
64
+ # Refresh the token this many seconds before it actually expires, so a
65
+ # token is never handed out mid-flight of expiring.
66
+ expiry_margin: float = DEFAULT_EXPIRY_MARGIN
67
+
68
+
69
+ def _discovery_url(config: ClientCredentials) -> str:
70
+ assert config.issuer is not None
71
+ if config.discovery_algorithm == "oauth2":
72
+ # RFC 8414: the well-known segment is inserted between host and path.
73
+ parts = urlsplit(config.issuer)
74
+ path = parts.path.rstrip("/")
75
+ return f"{parts.scheme}://{parts.netloc}/.well-known/oauth-authorization-server{path}"
76
+ return config.issuer.rstrip("/") + "/.well-known/openid-configuration"
77
+
78
+
79
+ def _token_request_args(config: ClientCredentials, token_endpoint: str) -> dict[str, Any]:
80
+ uses_clinia_auth_server = config.token_url is None and config.issuer is None
81
+ resource = config.resource or (CLINIA_OAUTH_RESOURCE if uses_clinia_auth_server else None)
82
+
83
+ data: dict[str, str] = {"grant_type": "client_credentials"}
84
+ if resource:
85
+ data["resource"] = resource
86
+ if config.audience:
87
+ data["audience"] = config.audience
88
+ if config.scope:
89
+ data["scope"] = config.scope
90
+
91
+ args: dict[str, Any] = {"url": token_endpoint, "data": data}
92
+ if config.auth_method == "basic":
93
+ args["auth"] = httpx.BasicAuth(config.client_id, config.client_secret)
94
+ else:
95
+ data["client_id"] = config.client_id
96
+ data["client_secret"] = config.client_secret
97
+ return args
98
+
99
+
100
+ def _parse_token_response(response: httpx.Response) -> tuple[str, float]:
101
+ """Returns the access token and the absolute time it should be refreshed."""
102
+ if response.is_error:
103
+ detail: str
104
+ try:
105
+ payload = response.json()
106
+ detail = str(payload.get("error", response.reason_phrase))
107
+ if payload.get("error_description"):
108
+ detail += f" — {payload['error_description']}"
109
+ except ValueError:
110
+ detail = response.reason_phrase
111
+ raise ContextEngineError(
112
+ f"client_credentials token request failed: {response.status_code} {detail}"
113
+ )
114
+ payload = response.json()
115
+ token = payload.get("access_token")
116
+ if not isinstance(token, str) or not token:
117
+ raise ContextEngineError(
118
+ "client_credentials token request failed: response carries no access_token"
119
+ )
120
+ ttl = float(payload["expires_in"]) if payload.get("expires_in") else DEFAULT_TTL
121
+ return token, ttl
122
+
123
+
124
+ def _parse_discovery_response(response: httpx.Response) -> str:
125
+ if response.is_error:
126
+ raise ContextEngineError(f"authorization server discovery failed: {response.status_code}")
127
+ endpoint = response.json().get("token_endpoint")
128
+ if not isinstance(endpoint, str) or not endpoint:
129
+ raise ContextEngineError(
130
+ "authorization server discovery failed: metadata carries no token_endpoint"
131
+ )
132
+ return endpoint
133
+
134
+
135
+ class _TokenState:
136
+ """Cached token plus the resolved token endpoint, shared by both providers."""
137
+
138
+ def __init__(self, config: ClientCredentials) -> None:
139
+ self.config = config
140
+ # Discovery only when an issuer is named and no explicit endpoint
141
+ # overrides it; with neither, Clinia's endpoint is the default.
142
+ if config.token_url or config.issuer is None:
143
+ self.token_endpoint: str | None = config.token_url or CLINIA_OAUTH_TOKEN_URL
144
+ else:
145
+ self.token_endpoint = None
146
+ self.token: str | None = None
147
+ # Absolute monotonic time at which the cached token is considered stale.
148
+ self.refresh_at = 0.0
149
+
150
+ def fresh_token(self) -> str | None:
151
+ if self.token is not None and time.monotonic() < self.refresh_at:
152
+ return self.token
153
+ return None
154
+
155
+ def store(self, token: str, ttl: float) -> str:
156
+ self.token = token
157
+ self.refresh_at = time.monotonic() + max(0.0, ttl - self.config.expiry_margin)
158
+ return token
159
+
160
+
161
+ class ClientCredentialsTokenProvider:
162
+ """Callable token provider backed by the OAuth2 client-credentials grant.
163
+
164
+ Caches the access token until it is within `expiry_margin` of expiry;
165
+ concurrent callers share one token request. When configured with an
166
+ `issuer`, the authorization server metadata is discovered once and
167
+ memoized.
168
+ """
169
+
170
+ def __init__(
171
+ self, config: ClientCredentials, *, http_client: httpx.Client | None = None
172
+ ) -> None:
173
+ self._state = _TokenState(config)
174
+ self._http_client = http_client
175
+ self._lock = threading.Lock()
176
+
177
+ def __call__(self) -> str:
178
+ token = self._state.fresh_token()
179
+ if token is not None:
180
+ return token
181
+ with self._lock:
182
+ token = self._state.fresh_token()
183
+ if token is not None:
184
+ return token
185
+ return self._fetch()
186
+
187
+ def _fetch(self) -> str:
188
+ client = self._http_client or httpx.Client()
189
+ try:
190
+ if self._state.token_endpoint is None:
191
+ response = client.get(_discovery_url(self._state.config))
192
+ self._state.token_endpoint = _parse_discovery_response(response)
193
+ args = _token_request_args(self._state.config, self._state.token_endpoint)
194
+ token, ttl = _parse_token_response(client.post(**args))
195
+ return self._state.store(token, ttl)
196
+ finally:
197
+ if self._http_client is None:
198
+ client.close()
199
+
200
+
201
+ class AsyncClientCredentialsTokenProvider:
202
+ """Async twin of `ClientCredentialsTokenProvider`."""
203
+
204
+ def __init__(
205
+ self, config: ClientCredentials, *, http_client: httpx.AsyncClient | None = None
206
+ ) -> None:
207
+ self._state = _TokenState(config)
208
+ self._http_client = http_client
209
+ self._lock = asyncio.Lock()
210
+
211
+ async def __call__(self) -> str:
212
+ token = self._state.fresh_token()
213
+ if token is not None:
214
+ return token
215
+ async with self._lock:
216
+ token = self._state.fresh_token()
217
+ if token is not None:
218
+ return token
219
+ return await self._fetch()
220
+
221
+ async def _fetch(self) -> str:
222
+ client = self._http_client or httpx.AsyncClient()
223
+ try:
224
+ if self._state.token_endpoint is None:
225
+ response = await client.get(_discovery_url(self._state.config))
226
+ self._state.token_endpoint = _parse_discovery_response(response)
227
+ args = _token_request_args(self._state.config, self._state.token_endpoint)
228
+ token, ttl = _parse_token_response(await client.post(**args))
229
+ return self._state.store(token, ttl)
230
+ finally:
231
+ if self._http_client is None:
232
+ await client.aclose()
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from .generated.models import ApiError
6
+
7
+ __all__ = [
8
+ "APIConnectionError",
9
+ "APIStatusError",
10
+ "APITimeoutError",
11
+ "ContextEngineError",
12
+ ]
13
+
14
+
15
+ class ContextEngineError(Exception):
16
+ """Base class for every error raised by this client."""
17
+
18
+
19
+ class APIConnectionError(ContextEngineError):
20
+ """The request never produced an HTTP response."""
21
+
22
+ def __init__(self, message: str, *, request: httpx.Request) -> None:
23
+ super().__init__(message)
24
+ self.request = request
25
+
26
+
27
+ class APITimeoutError(APIConnectionError):
28
+ def __init__(self, *, request: httpx.Request) -> None:
29
+ super().__init__("Request timed out.", request=request)
30
+
31
+
32
+ class APIStatusError(ContextEngineError):
33
+ """A non-2xx HTTP response.
34
+
35
+ `error` carries the server's parsed error envelope when the body is a valid
36
+ `ApiError`; `body` always carries the raw response text.
37
+ """
38
+
39
+ def __init__(self, *, response: httpx.Response, error: ApiError | None) -> None:
40
+ detail = f" — {error.type}: {error.message}" if error else ""
41
+ super().__init__(f"{response.status_code} {response.reason_phrase}{detail}")
42
+ self.response = response
43
+ self.status_code = response.status_code
44
+ self.error = error
45
+ self.body = response.text
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable, Mapping
4
+ from typing import Any, TypeVar, overload
5
+ from urllib.parse import quote
6
+
7
+ import httpx
8
+ from pydantic import BaseModel, TypeAdapter, ValidationError
9
+
10
+ from ._errors import APIConnectionError, APIStatusError, APITimeoutError
11
+ from .generated.models import ApiError
12
+
13
+ __all__ = ["AsyncHttpClient", "AsyncTokenProvider", "HttpClient", "TokenProvider"]
14
+
15
+ TokenProvider = Callable[[], str]
16
+ AsyncTokenProvider = Callable[[], Awaitable[str]]
17
+
18
+ DEFAULT_TIMEOUT = httpx.Timeout(60.0)
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ def _substitute_path(path: str, path_params: Mapping[str, Any] | None) -> str:
24
+ if not path_params:
25
+ return path
26
+ for name, value in path_params.items():
27
+ path = path.replace("{" + name + "}", quote(str(value), safe=""))
28
+ return path
29
+
30
+
31
+ def _prepare_query(query: Mapping[str, Any] | None) -> dict[str, Any] | None:
32
+ """Omitted (None) parameters are dropped rather than sent as empty values."""
33
+ if query is None:
34
+ return None
35
+ prepared = {name: value for name, value in query.items() if value is not None}
36
+ return prepared or None
37
+
38
+
39
+ def _prepare_json(body: Any) -> Any:
40
+ if isinstance(body, BaseModel):
41
+ return body.model_dump(mode="json", by_alias=True, exclude_unset=True)
42
+ return body
43
+
44
+
45
+ def _parse_response(response: httpx.Response, cast_to: TypeAdapter[T] | None) -> T | None:
46
+ if cast_to is None:
47
+ return None
48
+ # Operations that also declare a text response (e.g. a Markdown rendering
49
+ # behind a `format` query parameter) carry `str` in `cast_to`'s union, so a
50
+ # non-JSON response validates as its text.
51
+ if "json" in response.headers.get("Content-Type", ""):
52
+ return cast_to.validate_json(response.content)
53
+ return cast_to.validate_python(response.text)
54
+
55
+
56
+ def _status_error(response: httpx.Response) -> APIStatusError:
57
+ try:
58
+ error = ApiError.model_validate_json(response.content)
59
+ except ValidationError:
60
+ error = None
61
+ return APIStatusError(response=response, error=error)
62
+
63
+
64
+ class _RequestBuilder:
65
+ """Request construction shared by the sync and async clients."""
66
+
67
+ def __init__(self, base_url: str) -> None:
68
+ self._base_url = base_url.rstrip("/")
69
+
70
+ def build_args(
71
+ self,
72
+ method: str,
73
+ path: str,
74
+ *,
75
+ path_params: Mapping[str, Any] | None,
76
+ query: Mapping[str, Any] | None,
77
+ json: Any,
78
+ content: str | bytes | None,
79
+ content_type: str | None,
80
+ token: str | None,
81
+ ) -> dict[str, Any]:
82
+ headers: dict[str, str] = {}
83
+ if token:
84
+ headers["Authorization"] = f"Bearer {token}"
85
+ if content_type is not None:
86
+ headers["Content-Type"] = content_type
87
+
88
+ args: dict[str, Any] = {
89
+ "method": method,
90
+ "url": self._base_url + _substitute_path(path, path_params),
91
+ "headers": headers,
92
+ }
93
+ params = _prepare_query(query)
94
+ if params is not None:
95
+ args["params"] = params
96
+ if json is not None:
97
+ args["json"] = _prepare_json(json)
98
+ if content is not None:
99
+ args["content"] = content
100
+ return args
101
+
102
+
103
+ class HttpClient:
104
+ """Thin typed transport: one `request()` the generated resources call.
105
+
106
+ Owns an `httpx.Client` unless one is injected; injected clients are not
107
+ closed by `close()`.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ *,
113
+ base_url: str,
114
+ token_provider: TokenProvider | None = None,
115
+ timeout: float | httpx.Timeout | None = None,
116
+ http_client: httpx.Client | None = None,
117
+ ) -> None:
118
+ self._builder = _RequestBuilder(base_url)
119
+ self._token_provider = token_provider
120
+ self._owns_client = http_client is None
121
+ self._client = http_client or httpx.Client(timeout=timeout or DEFAULT_TIMEOUT)
122
+
123
+ @overload
124
+ def request(
125
+ self,
126
+ method: str,
127
+ path: str,
128
+ *,
129
+ cast_to: TypeAdapter[T],
130
+ path_params: Mapping[str, Any] | None = None,
131
+ query: Mapping[str, Any] | None = None,
132
+ json: Any = None,
133
+ content: str | bytes | None = None,
134
+ content_type: str | None = None,
135
+ ) -> T: ...
136
+
137
+ @overload
138
+ def request(
139
+ self,
140
+ method: str,
141
+ path: str,
142
+ *,
143
+ cast_to: None,
144
+ path_params: Mapping[str, Any] | None = None,
145
+ query: Mapping[str, Any] | None = None,
146
+ json: Any = None,
147
+ content: str | bytes | None = None,
148
+ content_type: str | None = None,
149
+ ) -> None: ...
150
+
151
+ def request(
152
+ self,
153
+ method: str,
154
+ path: str,
155
+ *,
156
+ cast_to: TypeAdapter[T] | None,
157
+ path_params: Mapping[str, Any] | None = None,
158
+ query: Mapping[str, Any] | None = None,
159
+ json: Any = None,
160
+ content: str | bytes | None = None,
161
+ content_type: str | None = None,
162
+ ) -> T | None:
163
+ token = self._token_provider() if self._token_provider else None
164
+ args = self._builder.build_args(
165
+ method,
166
+ path,
167
+ path_params=path_params,
168
+ query=query,
169
+ json=json,
170
+ content=content,
171
+ content_type=content_type,
172
+ token=token,
173
+ )
174
+ try:
175
+ response = self._client.request(**args)
176
+ except httpx.TimeoutException as err:
177
+ raise APITimeoutError(request=err.request) from err
178
+ except httpx.TransportError as err:
179
+ raise APIConnectionError(str(err), request=err.request) from err
180
+ if response.is_error:
181
+ raise _status_error(response)
182
+ return _parse_response(response, cast_to)
183
+
184
+ def close(self) -> None:
185
+ if self._owns_client:
186
+ self._client.close()
187
+
188
+ def __enter__(self) -> HttpClient:
189
+ return self
190
+
191
+ def __exit__(self, *exc_info: object) -> None:
192
+ self.close()
193
+
194
+
195
+ class AsyncHttpClient:
196
+ """Async twin of `HttpClient`, backed by `httpx.AsyncClient`."""
197
+
198
+ def __init__(
199
+ self,
200
+ *,
201
+ base_url: str,
202
+ token_provider: AsyncTokenProvider | None = None,
203
+ timeout: float | httpx.Timeout | None = None,
204
+ http_client: httpx.AsyncClient | None = None,
205
+ ) -> None:
206
+ self._builder = _RequestBuilder(base_url)
207
+ self._token_provider = token_provider
208
+ self._owns_client = http_client is None
209
+ self._client = http_client or httpx.AsyncClient(timeout=timeout or DEFAULT_TIMEOUT)
210
+
211
+ @overload
212
+ async def request(
213
+ self,
214
+ method: str,
215
+ path: str,
216
+ *,
217
+ cast_to: TypeAdapter[T],
218
+ path_params: Mapping[str, Any] | None = None,
219
+ query: Mapping[str, Any] | None = None,
220
+ json: Any = None,
221
+ content: str | bytes | None = None,
222
+ content_type: str | None = None,
223
+ ) -> T: ...
224
+
225
+ @overload
226
+ async def request(
227
+ self,
228
+ method: str,
229
+ path: str,
230
+ *,
231
+ cast_to: None,
232
+ path_params: Mapping[str, Any] | None = None,
233
+ query: Mapping[str, Any] | None = None,
234
+ json: Any = None,
235
+ content: str | bytes | None = None,
236
+ content_type: str | None = None,
237
+ ) -> None: ...
238
+
239
+ async def request(
240
+ self,
241
+ method: str,
242
+ path: str,
243
+ *,
244
+ cast_to: TypeAdapter[T] | None,
245
+ path_params: Mapping[str, Any] | None = None,
246
+ query: Mapping[str, Any] | None = None,
247
+ json: Any = None,
248
+ content: str | bytes | None = None,
249
+ content_type: str | None = None,
250
+ ) -> T | None:
251
+ token = await self._token_provider() if self._token_provider else None
252
+ args = self._builder.build_args(
253
+ method,
254
+ path,
255
+ path_params=path_params,
256
+ query=query,
257
+ json=json,
258
+ content=content,
259
+ content_type=content_type,
260
+ token=token,
261
+ )
262
+ try:
263
+ response = await self._client.request(**args)
264
+ except httpx.TimeoutException as err:
265
+ raise APITimeoutError(request=err.request) from err
266
+ except httpx.TransportError as err:
267
+ raise APIConnectionError(str(err), request=err.request) from err
268
+ if response.is_error:
269
+ raise _status_error(response)
270
+ return _parse_response(response, cast_to)
271
+
272
+ async def aclose(self) -> None:
273
+ if self._owns_client:
274
+ await self._client.aclose()
275
+
276
+ async def __aenter__(self) -> AsyncHttpClient:
277
+ return self
278
+
279
+ async def __aexit__(self, *exc_info: object) -> None:
280
+ await self.aclose()
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from ._auth import (
6
+ AsyncClientCredentialsTokenProvider,
7
+ ClientCredentials,
8
+ ClientCredentialsTokenProvider,
9
+ )
10
+ from ._http import AsyncHttpClient, AsyncTokenProvider, HttpClient, TokenProvider
11
+ from .resources import (
12
+ AsyncGraphResource,
13
+ AsyncInfoResource,
14
+ AsyncIngestResource,
15
+ AsyncPatientsResource,
16
+ AsyncSessionsResource,
17
+ AsyncVFSResource,
18
+ GraphResource,
19
+ InfoResource,
20
+ IngestResource,
21
+ PatientsResource,
22
+ SessionsResource,
23
+ VFSResource,
24
+ )
25
+
26
+ __all__ = ["AsyncContextEngineClient", "ContextEngineClient"]
27
+
28
+
29
+ class ContextEngineClient:
30
+ """Synchronous client for the Context Engine API.
31
+
32
+ `auth` accepts either a `ClientCredentials` config — the common case, the
33
+ client then handles token acquisition, caching, and refresh — or a token
34
+ callable for full control (a token from elsewhere, a different grant, or a
35
+ custom refresh strategy), called per request and attached as
36
+ `Authorization: Bearer <token>`. Omit for an unsecured server.
37
+
38
+ Owns network resources; use it as a context manager or call `close()`.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ *,
44
+ base_url: str,
45
+ auth: ClientCredentials | TokenProvider | None = None,
46
+ timeout: float | httpx.Timeout | None = None,
47
+ http_client: httpx.Client | None = None,
48
+ ) -> None:
49
+ token_provider: TokenProvider | None
50
+ if isinstance(auth, ClientCredentials):
51
+ token_provider = ClientCredentialsTokenProvider(auth)
52
+ else:
53
+ token_provider = auth
54
+
55
+ self.http = HttpClient(
56
+ base_url=base_url,
57
+ token_provider=token_provider,
58
+ timeout=timeout,
59
+ http_client=http_client,
60
+ )
61
+ self.graph = GraphResource(self.http)
62
+ self.info = InfoResource(self.http)
63
+ self.ingest = IngestResource(self.http)
64
+ self.patients = PatientsResource(self.http)
65
+ self.sessions = SessionsResource(self.http)
66
+ self.vfs = VFSResource(self.http)
67
+
68
+ def close(self) -> None:
69
+ self.http.close()
70
+
71
+ def __enter__(self) -> ContextEngineClient:
72
+ return self
73
+
74
+ def __exit__(self, *exc_info: object) -> None:
75
+ self.close()
76
+
77
+
78
+ class AsyncContextEngineClient:
79
+ """Async twin of `ContextEngineClient`."""
80
+
81
+ def __init__(
82
+ self,
83
+ *,
84
+ base_url: str,
85
+ auth: ClientCredentials | AsyncTokenProvider | None = None,
86
+ timeout: float | httpx.Timeout | None = None,
87
+ http_client: httpx.AsyncClient | None = None,
88
+ ) -> None:
89
+ token_provider: AsyncTokenProvider | None
90
+ if isinstance(auth, ClientCredentials):
91
+ token_provider = AsyncClientCredentialsTokenProvider(auth)
92
+ else:
93
+ token_provider = auth
94
+
95
+ self.http = AsyncHttpClient(
96
+ base_url=base_url,
97
+ token_provider=token_provider,
98
+ timeout=timeout,
99
+ http_client=http_client,
100
+ )
101
+ self.graph = AsyncGraphResource(self.http)
102
+ self.info = AsyncInfoResource(self.http)
103
+ self.ingest = AsyncIngestResource(self.http)
104
+ self.patients = AsyncPatientsResource(self.http)
105
+ self.sessions = AsyncSessionsResource(self.http)
106
+ self.vfs = AsyncVFSResource(self.http)
107
+
108
+ async def aclose(self) -> None:
109
+ await self.http.aclose()
110
+
111
+ async def __aenter__(self) -> AsyncContextEngineClient:
112
+ return self
113
+
114
+ async def __aexit__(self, *exc_info: object) -> None:
115
+ await self.aclose()
@@ -0,0 +1,3 @@
1
+ from . import models as models
2
+
3
+ __all__ = ["models"]