termix-sdk 0.1.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.
Files changed (148) hide show
  1. termix_sdk/__init__.py +57 -0
  2. termix_sdk/_api_requestor.py +357 -0
  3. termix_sdk/_async_client.py +217 -0
  4. termix_sdk/_auth.py +225 -0
  5. termix_sdk/_client.py +237 -0
  6. termix_sdk/_client_options.py +114 -0
  7. termix_sdk/_error.py +156 -0
  8. termix_sdk/_http_client.py +387 -0
  9. termix_sdk/_object.py +181 -0
  10. termix_sdk/_request_options.py +44 -0
  11. termix_sdk/_response.py +106 -0
  12. termix_sdk/_service.py +153 -0
  13. termix_sdk/_session.py +214 -0
  14. termix_sdk/_sse.py +79 -0
  15. termix_sdk/_util.py +93 -0
  16. termix_sdk/_version.py +6 -0
  17. termix_sdk/models/__init__.py +3 -0
  18. termix_sdk/models/ai.py +59 -0
  19. termix_sdk/models/alerts.py +115 -0
  20. termix_sdk/models/api_keys.py +29 -0
  21. termix_sdk/models/audit.py +21 -0
  22. termix_sdk/models/automations.py +79 -0
  23. termix_sdk/models/credentials.py +102 -0
  24. termix_sdk/models/dashboard.py +36 -0
  25. termix_sdk/models/database.py +38 -0
  26. termix_sdk/models/docker.py +106 -0
  27. termix_sdk/models/encryption.py +35 -0
  28. termix_sdk/models/file_manager.py +234 -0
  29. termix_sdk/models/fleets.py +75 -0
  30. termix_sdk/models/guacamole.py +24 -0
  31. termix_sdk/models/homepage.py +46 -0
  32. termix_sdk/models/host_file_manager.py +34 -0
  33. termix_sdk/models/hosts.py +608 -0
  34. termix_sdk/models/instance_settings.py +215 -0
  35. termix_sdk/models/metrics.py +253 -0
  36. termix_sdk/models/network_topology.py +14 -0
  37. termix_sdk/models/open_tabs.py +43 -0
  38. termix_sdk/models/preferences.py +105 -0
  39. termix_sdk/models/proxmox.py +11 -0
  40. termix_sdk/models/proxmox_stats.py +37 -0
  41. termix_sdk/models/rbac.py +105 -0
  42. termix_sdk/models/session_logs.py +30 -0
  43. termix_sdk/models/session_sharing.py +34 -0
  44. termix_sdk/models/snippets.py +84 -0
  45. termix_sdk/models/sso.py +31 -0
  46. termix_sdk/models/sync.py +27 -0
  47. termix_sdk/models/system.py +22 -0
  48. termix_sdk/models/tailscale.py +15 -0
  49. termix_sdk/models/terminal.py +27 -0
  50. termix_sdk/models/termix_id.py +91 -0
  51. termix_sdk/models/tmux.py +15 -0
  52. termix_sdk/models/tunnel.py +36 -0
  53. termix_sdk/models/tunnel_presets.py +22 -0
  54. termix_sdk/models/user_admin.py +60 -0
  55. termix_sdk/models/users.py +142 -0
  56. termix_sdk/models/vault.py +26 -0
  57. termix_sdk/models/webauthn.py +32 -0
  58. termix_sdk/models/workspaces.py +95 -0
  59. termix_sdk/py.typed +0 -0
  60. termix_sdk/resources/__init__.py +3 -0
  61. termix_sdk/resources/ai.py +457 -0
  62. termix_sdk/resources/alerts.py +562 -0
  63. termix_sdk/resources/api_keys.py +113 -0
  64. termix_sdk/resources/audit.py +120 -0
  65. termix_sdk/resources/automations.py +332 -0
  66. termix_sdk/resources/credentials.py +648 -0
  67. termix_sdk/resources/dashboard.py +287 -0
  68. termix_sdk/resources/database.py +214 -0
  69. termix_sdk/resources/docker.py +689 -0
  70. termix_sdk/resources/encryption.py +145 -0
  71. termix_sdk/resources/file_manager.py +1468 -0
  72. termix_sdk/resources/fleets.py +535 -0
  73. termix_sdk/resources/guacamole.py +133 -0
  74. termix_sdk/resources/homepage.py +364 -0
  75. termix_sdk/resources/host_file_manager.py +331 -0
  76. termix_sdk/resources/hosts.py +1047 -0
  77. termix_sdk/resources/instance_settings.py +1381 -0
  78. termix_sdk/resources/metrics.py +1917 -0
  79. termix_sdk/resources/network_topology.py +82 -0
  80. termix_sdk/resources/open_tabs.py +233 -0
  81. termix_sdk/resources/preferences.py +376 -0
  82. termix_sdk/resources/proxmox.py +117 -0
  83. termix_sdk/resources/proxmox_stats.py +279 -0
  84. termix_sdk/resources/rbac.py +762 -0
  85. termix_sdk/resources/session_logs.py +208 -0
  86. termix_sdk/resources/session_sharing.py +197 -0
  87. termix_sdk/resources/snippets.py +495 -0
  88. termix_sdk/resources/sso.py +223 -0
  89. termix_sdk/resources/sync.py +183 -0
  90. termix_sdk/resources/system.py +116 -0
  91. termix_sdk/resources/tailscale.py +50 -0
  92. termix_sdk/resources/terminal.py +278 -0
  93. termix_sdk/resources/termix_id.py +620 -0
  94. termix_sdk/resources/tmux.py +516 -0
  95. termix_sdk/resources/tunnel.py +217 -0
  96. termix_sdk/resources/tunnel_presets.py +152 -0
  97. termix_sdk/resources/user_admin.py +356 -0
  98. termix_sdk/resources/users.py +884 -0
  99. termix_sdk/resources/vault.py +155 -0
  100. termix_sdk/resources/webauthn.py +269 -0
  101. termix_sdk/resources/workspaces.py +403 -0
  102. termix_sdk/types/__init__.py +3 -0
  103. termix_sdk/types/ai.py +41 -0
  104. termix_sdk/types/alerts.py +56 -0
  105. termix_sdk/types/api_keys.py +16 -0
  106. termix_sdk/types/audit.py +19 -0
  107. termix_sdk/types/automations.py +38 -0
  108. termix_sdk/types/credentials.py +86 -0
  109. termix_sdk/types/dashboard.py +39 -0
  110. termix_sdk/types/database.py +20 -0
  111. termix_sdk/types/docker.py +60 -0
  112. termix_sdk/types/encryption.py +11 -0
  113. termix_sdk/types/file_manager.py +181 -0
  114. termix_sdk/types/fleets.py +53 -0
  115. termix_sdk/types/guacamole.py +27 -0
  116. termix_sdk/types/homepage.py +46 -0
  117. termix_sdk/types/host_file_manager.py +40 -0
  118. termix_sdk/types/hosts.py +355 -0
  119. termix_sdk/types/instance_settings.py +114 -0
  120. termix_sdk/types/metrics.py +130 -0
  121. termix_sdk/types/network_topology.py +14 -0
  122. termix_sdk/types/open_tabs.py +30 -0
  123. termix_sdk/types/preferences.py +68 -0
  124. termix_sdk/types/proxmox.py +18 -0
  125. termix_sdk/types/proxmox_stats.py +27 -0
  126. termix_sdk/types/rbac.py +53 -0
  127. termix_sdk/types/session_logs.py +14 -0
  128. termix_sdk/types/session_sharing.py +21 -0
  129. termix_sdk/types/snippets.py +52 -0
  130. termix_sdk/types/sso.py +33 -0
  131. termix_sdk/types/sync.py +27 -0
  132. termix_sdk/types/system.py +19 -0
  133. termix_sdk/types/tailscale.py +11 -0
  134. termix_sdk/types/terminal.py +31 -0
  135. termix_sdk/types/termix_id.py +51 -0
  136. termix_sdk/types/tmux.py +54 -0
  137. termix_sdk/types/tunnel.py +38 -0
  138. termix_sdk/types/tunnel_presets.py +24 -0
  139. termix_sdk/types/user_admin.py +36 -0
  140. termix_sdk/types/users.py +81 -0
  141. termix_sdk/types/vault.py +42 -0
  142. termix_sdk/types/webauthn.py +31 -0
  143. termix_sdk/types/workspaces.py +35 -0
  144. termix_sdk-0.1.0.dist-info/METADATA +191 -0
  145. termix_sdk-0.1.0.dist-info/RECORD +148 -0
  146. termix_sdk-0.1.0.dist-info/WHEEL +4 -0
  147. termix_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  148. termix_sdk-0.1.0.dist-info/licenses/NOTICE +43 -0
termix_sdk/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ """Python client SDK for the Termix REST API.
2
+
3
+ See docs/sdk-plan.md for the full design (this file's public surface grows
4
+ as tools/sdk-gen generates resources on top of the core in this module).
5
+ """
6
+
7
+ from ._async_client import AsyncTermixClient
8
+ from ._auth import AsyncPendingTOTP, PendingTOTP
9
+ from ._client import TermixClient
10
+ from ._error import (
11
+ APIConnectionError,
12
+ APIStatusError,
13
+ AuthenticationError,
14
+ DataLockedError,
15
+ InvalidRequestError,
16
+ NotFoundError,
17
+ PermissionError,
18
+ RateLimitError,
19
+ ServerError,
20
+ SessionExpiredError,
21
+ TermixError,
22
+ TOTPRequiredError,
23
+ )
24
+ from ._object import TermixObject
25
+ from ._request_options import RequestOptions
26
+ from ._response import SSEEvent, TermixResponse, TermixStreamResponse
27
+ from ._session import SessionHandle, async_ssh_session, ssh_session
28
+ from ._version import SPEC_VERSION, __version__
29
+
30
+ __all__ = [
31
+ "__version__",
32
+ "SPEC_VERSION",
33
+ "TermixClient",
34
+ "AsyncTermixClient",
35
+ "PendingTOTP",
36
+ "AsyncPendingTOTP",
37
+ "TermixObject",
38
+ "TermixResponse",
39
+ "TermixStreamResponse",
40
+ "SSEEvent",
41
+ "RequestOptions",
42
+ "SessionHandle",
43
+ "ssh_session",
44
+ "async_ssh_session",
45
+ "TermixError",
46
+ "APIConnectionError",
47
+ "APIStatusError",
48
+ "InvalidRequestError",
49
+ "AuthenticationError",
50
+ "TOTPRequiredError",
51
+ "SessionExpiredError",
52
+ "PermissionError",
53
+ "NotFoundError",
54
+ "RateLimitError",
55
+ "DataLockedError",
56
+ "ServerError",
57
+ ]
@@ -0,0 +1,357 @@
1
+ # Adapted from stripe-python (stripe/_api_requestor.py), MIT License,
2
+ # Copyright (c) 2010-2018 Stripe. See NOTICE for the full license text.
3
+ """Builds and sends one API call: resolves which service's base URL to hit,
4
+ attaches auth, serializes the body, and turns a non-2xx response into the
5
+ right `TermixError` subclass.
6
+
7
+ Diverges from stripe's requestor in the ways docs/sdk-plan.md calls out:
8
+ no form-encoding (`_encode.py` doesn't exist here — bodies are plain JSON
9
+ or httpx-native multipart), no idempotency keys, no API-version header,
10
+ and URL resolution goes through `ClientOptions.base_url_for(service)`
11
+ instead of a single fixed API base.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json as _json
17
+ from collections.abc import AsyncIterator, Iterator, Mapping
18
+ from typing import Any
19
+ from urllib.parse import urljoin
20
+
21
+ from . import _util
22
+ from ._client_options import ClientOptions
23
+ from ._error import APIConnectionError, error_for_status
24
+ from ._http_client import (
25
+ RETRIABLE_STATUS_CODES,
26
+ AsyncHTTPClient,
27
+ AsyncHTTPXClient,
28
+ HTTPClient,
29
+ HTTPXClient,
30
+ ShouldRetry,
31
+ )
32
+ from ._request_options import RequestOptions, request_headers
33
+ from ._response import SSEEvent, TermixResponse, TermixStreamResponse
34
+ from ._sse import aiter_sse_events, iter_sse_events
35
+
36
+ USER_AGENT = "termix-sdk-python"
37
+
38
+
39
+ def _should_retry(method: str) -> ShouldRetry:
40
+ """GET/HEAD are safe to retry on a retryable status too; other verbs
41
+ only retry on a connection failure (the request never reached the
42
+ server, so nothing has happened yet) — retrying a POST after a
43
+ completed-but-slow-to-answer request could double an action like
44
+ `POST /fleets/{id}/execute`.
45
+ """
46
+ method = method.upper()
47
+
48
+ def predicate(_num_retries: int, status: int | None, _headers: Mapping[str, str]) -> bool:
49
+ if status is None:
50
+ return True # connection failure: always safe to retry
51
+ if method in ("GET", "HEAD"):
52
+ return status in RETRIABLE_STATUS_CODES
53
+ return False
54
+
55
+ return predicate
56
+
57
+
58
+ class APIRequestor:
59
+ def __init__(self, options: ClientOptions, http_client: HTTPClient | None = None) -> None:
60
+ self._options = options
61
+ self._http_client = http_client or HTTPXClient(
62
+ verify=options.verify, timeout=options.timeout
63
+ )
64
+
65
+ def close(self) -> None:
66
+ self._http_client.close()
67
+
68
+ def _url_for(self, service: str | None, path: str) -> str:
69
+ base = self._options.base_url_for(service)
70
+ return urljoin(base + "/", path.lstrip("/"))
71
+
72
+ def _headers(self, options: RequestOptions | None) -> dict:
73
+ headers = {
74
+ "Accept": "application/json",
75
+ "User-Agent": USER_AGENT,
76
+ }
77
+ bearer_token = self._options.bearer_token
78
+ if bearer_token is not None:
79
+ headers["Authorization"] = f"Bearer {bearer_token}"
80
+ headers.update(self._options.default_headers)
81
+ headers.update(request_headers(options))
82
+ return headers
83
+
84
+ def request(
85
+ self,
86
+ method: str,
87
+ path: str,
88
+ *,
89
+ service: str | None = None,
90
+ query: Mapping[str, Any] | None = None,
91
+ json_body: Any | None = None,
92
+ files: Mapping[str, Any] | None = None,
93
+ options: RequestOptions | None = None,
94
+ ) -> TermixResponse | None:
95
+ url = self._url_for(service, path)
96
+ headers = self._headers(options)
97
+ timeout = (options or {}).get("timeout", self._options.timeout)
98
+ max_retries = (options or {}).get("max_network_retries", self._options.max_network_retries)
99
+
100
+ _util.log_debug("Sending request", method=method, url=url)
101
+ content, status_code, resp_headers = self._http_client.request_with_retries(
102
+ method,
103
+ url,
104
+ headers=headers,
105
+ params=query,
106
+ json=json_body if files is None else None,
107
+ data=json_body if files is not None else None,
108
+ files=files,
109
+ timeout=timeout,
110
+ max_retries=max_retries,
111
+ should_retry=_should_retry(method),
112
+ )
113
+ return self._interpret_response(status_code, content, resp_headers)
114
+
115
+ def request_stream(
116
+ self,
117
+ method: str,
118
+ path: str,
119
+ *,
120
+ service: str | None = None,
121
+ query: Mapping[str, Any] | None = None,
122
+ json_body: Any | None = None,
123
+ options: RequestOptions | None = None,
124
+ ) -> TermixStreamResponse:
125
+ """For `application/octet-stream` responses (file downloads). Never
126
+ retried — see `HTTPClient.request_stream`.
127
+ """
128
+ url = self._url_for(service, path)
129
+ headers = self._headers(options)
130
+ timeout = (options or {}).get("timeout", self._options.timeout)
131
+
132
+ iterator, status_code, resp_headers = self._http_client.request_stream(
133
+ method, url, headers=headers, params=query, json=json_body, timeout=timeout
134
+ )
135
+ if status_code >= 400:
136
+ body = b"".join(iterator)
137
+ self._interpret_response(status_code, body, resp_headers) # raises
138
+ return TermixStreamResponse(iterator, status_code, resp_headers)
139
+
140
+ def request_sse(
141
+ self,
142
+ method: str,
143
+ path: str,
144
+ *,
145
+ service: str | None = None,
146
+ query: Mapping[str, Any] | None = None,
147
+ json_body: Any | None = None,
148
+ options: RequestOptions | None = None,
149
+ ) -> Iterator[SSEEvent]:
150
+ """For the 3 `text/event-stream` endpoints (docs/sdk-plan.md
151
+ section 6, item 2 / `_sse.py`). Never retried, same as
152
+ `request_stream` — a partially-streamed connection can't be
153
+ safely replayed.
154
+ """
155
+ url = self._url_for(service, path)
156
+ headers = self._headers(options)
157
+ headers["Accept"] = "text/event-stream"
158
+ timeout = (options or {}).get("timeout", self._options.timeout)
159
+
160
+ iterator, status_code, resp_headers = self._http_client.request_stream(
161
+ method, url, headers=headers, params=query, json=json_body, timeout=timeout
162
+ )
163
+ if status_code >= 400:
164
+ body = b"".join(iterator)
165
+ self._interpret_response(status_code, body, resp_headers) # raises
166
+ return iter_sse_events(iterator)
167
+
168
+ def _interpret_response(
169
+ self,
170
+ status_code: int,
171
+ content: bytes,
172
+ headers: Mapping[str, str],
173
+ ) -> TermixResponse | None:
174
+ if status_code == 204 or not content:
175
+ if status_code >= 400:
176
+ self._raise_for_status(status_code, {}, content, headers)
177
+ return None
178
+
179
+ content_type = headers.get("content-type", "")
180
+ if "application/json" not in content_type:
181
+ # A handful of endpoints (e.g. RSS/health text, some error
182
+ # fallbacks) answer without a JSON content-type even on success.
183
+ # We still try to parse — Termix's own error middleware always
184
+ # sends JSON regardless of what a route declares — and fall
185
+ # back to the raw text if that fails.
186
+ try:
187
+ data = _json.loads(content)
188
+ except ValueError:
189
+ if status_code >= 400:
190
+ self._raise_for_status(status_code, {}, content, headers)
191
+ return TermixResponse(content, None, status_code, headers)
192
+ else:
193
+ try:
194
+ data = _json.loads(content)
195
+ except ValueError as exc:
196
+ raise APIConnectionError(
197
+ f"Termix returned a response with an "
198
+ f"application/json content-type that wasn't valid "
199
+ f"JSON: {exc}"
200
+ ) from exc
201
+
202
+ if status_code >= 400:
203
+ body = data if isinstance(data, dict) else {}
204
+ self._raise_for_status(status_code, body, content, headers)
205
+
206
+ return TermixResponse(content, data, status_code, headers)
207
+
208
+ def _raise_for_status(
209
+ self,
210
+ status_code: int,
211
+ body: dict,
212
+ raw_content: bytes,
213
+ headers: Mapping[str, str],
214
+ ) -> None:
215
+ message = body.get("error") if isinstance(body.get("error"), str) else None
216
+ if message is None:
217
+ message = f"Termix API returned status {status_code} with no error message"
218
+ code = body.get("code") if isinstance(body.get("code"), str) else None
219
+ raise error_for_status(
220
+ status_code,
221
+ message=message,
222
+ code=code,
223
+ body=body,
224
+ http_body=raw_content.decode("utf-8", errors="replace") if raw_content else None,
225
+ headers=dict(headers),
226
+ )
227
+
228
+
229
+ class AsyncAPIRequestor:
230
+ """Async mirror of `APIRequestor`, used by `AsyncTermixClient`. Response
231
+ interpretation and error-raising are pure functions of
232
+ (status, content, headers) with nothing async about them, so they're
233
+ delegated to a plain `APIRequestor` instance instead of duplicating
234
+ that logic.
235
+ """
236
+
237
+ def __init__(self, options: ClientOptions, http_client: AsyncHTTPClient | None = None) -> None:
238
+ self._http_client = http_client or AsyncHTTPXClient(
239
+ verify=options.verify, timeout=options.timeout
240
+ )
241
+ self._sync_delegate = APIRequestor(options, http_client=_NullHTTPClient())
242
+
243
+ @property
244
+ def _options(self) -> ClientOptions:
245
+ # `self._sync_delegate._options` is the single source of truth —
246
+ # `_auth.py` swaps in a freshly-authenticated ClientOptions after
247
+ # login/TOTP by assigning `requestor._options = ...`, and that has
248
+ # to reach `_headers()`/`_url_for()` below, which read through the
249
+ # delegate. A plain instance attribute here would silently keep
250
+ # pointing at the pre-login (anonymous) options instead.
251
+ return self._sync_delegate._options
252
+
253
+ @_options.setter
254
+ def _options(self, value: ClientOptions) -> None:
255
+ self._sync_delegate._options = value
256
+
257
+ async def close(self) -> None:
258
+ await self._http_client.close()
259
+
260
+ def _url_for(self, service: str | None, path: str) -> str:
261
+ return self._sync_delegate._url_for(service, path)
262
+
263
+ def _headers(self, options: RequestOptions | None) -> dict:
264
+ return self._sync_delegate._headers(options)
265
+
266
+ async def request(
267
+ self,
268
+ method: str,
269
+ path: str,
270
+ *,
271
+ service: str | None = None,
272
+ query: Mapping[str, Any] | None = None,
273
+ json_body: Any | None = None,
274
+ files: Mapping[str, Any] | None = None,
275
+ options: RequestOptions | None = None,
276
+ ) -> TermixResponse | None:
277
+ url = self._url_for(service, path)
278
+ headers = self._headers(options)
279
+ timeout = (options or {}).get("timeout", self._options.timeout)
280
+ max_retries = (options or {}).get("max_network_retries", self._options.max_network_retries)
281
+
282
+ _util.log_debug("Sending request", method=method, url=url)
283
+ content, status_code, resp_headers = await self._http_client.request_with_retries(
284
+ method,
285
+ url,
286
+ headers=headers,
287
+ params=query,
288
+ json=json_body if files is None else None,
289
+ data=json_body if files is not None else None,
290
+ files=files,
291
+ timeout=timeout,
292
+ max_retries=max_retries,
293
+ should_retry=_should_retry(method),
294
+ )
295
+ return self._sync_delegate._interpret_response(status_code, content, resp_headers)
296
+
297
+ async def request_stream(
298
+ self,
299
+ method: str,
300
+ path: str,
301
+ *,
302
+ service: str | None = None,
303
+ query: Mapping[str, Any] | None = None,
304
+ json_body: Any | None = None,
305
+ options: RequestOptions | None = None,
306
+ ):
307
+ from ._response import AsyncTermixStreamResponse
308
+
309
+ url = self._url_for(service, path)
310
+ headers = self._headers(options)
311
+ timeout = (options or {}).get("timeout", self._options.timeout)
312
+
313
+ aiterator, status_code, resp_headers = await self._http_client.request_stream(
314
+ method, url, headers=headers, params=query, json=json_body, timeout=timeout
315
+ )
316
+ if status_code >= 400:
317
+ chunks = [chunk async for chunk in aiterator]
318
+ body = b"".join(chunks)
319
+ self._sync_delegate._interpret_response(status_code, body, resp_headers) # raises
320
+ return AsyncTermixStreamResponse(aiterator, status_code, resp_headers)
321
+
322
+ async def request_sse(
323
+ self,
324
+ method: str,
325
+ path: str,
326
+ *,
327
+ service: str | None = None,
328
+ query: Mapping[str, Any] | None = None,
329
+ json_body: Any | None = None,
330
+ options: RequestOptions | None = None,
331
+ ) -> AsyncIterator[SSEEvent]:
332
+ """Async mirror of `APIRequestor.request_sse()` — see its docstring."""
333
+ url = self._url_for(service, path)
334
+ headers = self._headers(options)
335
+ headers["Accept"] = "text/event-stream"
336
+ timeout = (options or {}).get("timeout", self._options.timeout)
337
+
338
+ aiterator, status_code, resp_headers = await self._http_client.request_stream(
339
+ method, url, headers=headers, params=query, json=json_body, timeout=timeout
340
+ )
341
+ if status_code >= 400:
342
+ chunks = [chunk async for chunk in aiterator]
343
+ body = b"".join(chunks)
344
+ self._sync_delegate._interpret_response(status_code, body, resp_headers) # raises
345
+ return aiter_sse_events(aiterator)
346
+
347
+
348
+ class _NullHTTPClient(HTTPClient):
349
+ """Never used to actually send anything — `AsyncAPIRequestor` only
350
+ borrows `APIRequestor`'s pure response-parsing methods, never its
351
+ `request`/`request_with_retries`. Keeping `APIRequestor.__init__` from
352
+ constructing a real (sync) `HTTPXClient` here avoids opening a second,
353
+ unused connection pool per `AsyncTermixClient`.
354
+ """
355
+
356
+ def __init__(self) -> None: # no super().__init__: nothing needs verify/timeout
357
+ pass
@@ -0,0 +1,217 @@
1
+ # Adapted from stripe-python (stripe/_stripe_client.py), MIT License,
2
+ # Copyright (c) 2010-2018 Stripe. See NOTICE for the full license text.
3
+ """AsyncTermixClient: the async entry point, kept as its own class rather
4
+ than `x_async()` methods on `TermixClient` (docs/sdk-plan.md section 2,
5
+ item 7). See `_client.py` for the sync twin this mirrors line for line.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from ._api_requestor import AsyncAPIRequestor
14
+ from ._client_options import ClientOptions
15
+ from ._http_client import AsyncHTTPClient
16
+ from ._request_options import RequestOptions
17
+ from ._response import TermixResponse
18
+
19
+ # --- generated resource imports start ---
20
+
21
+ from .resources import ai as _ai
22
+ from .resources import alerts as _alerts
23
+ from .resources import api_keys as _api_keys
24
+ from .resources import audit as _audit
25
+ from .resources import automations as _automations
26
+ from .resources import credentials as _credentials
27
+ from .resources import dashboard as _dashboard
28
+ from .resources import database as _database
29
+ from .resources import docker as _docker
30
+ from .resources import encryption as _encryption
31
+ from .resources import file_manager as _file_manager
32
+ from .resources import fleets as _fleets
33
+ from .resources import guacamole as _guacamole
34
+ from .resources import homepage as _homepage
35
+ from .resources import host_file_manager as _host_file_manager
36
+ from .resources import hosts as _hosts
37
+ from .resources import instance_settings as _instance_settings
38
+ from .resources import metrics as _metrics
39
+ from .resources import network_topology as _network_topology
40
+ from .resources import open_tabs as _open_tabs
41
+ from .resources import preferences as _preferences
42
+ from .resources import proxmox as _proxmox
43
+ from .resources import proxmox_stats as _proxmox_stats
44
+ from .resources import rbac as _rbac
45
+ from .resources import session_logs as _session_logs
46
+ from .resources import session_sharing as _session_sharing
47
+ from .resources import snippets as _snippets
48
+ from .resources import sso as _sso
49
+ from .resources import sync as _sync
50
+ from .resources import system as _system
51
+ from .resources import tailscale as _tailscale
52
+ from .resources import terminal as _terminal
53
+ from .resources import termix_id as _termix_id
54
+ from .resources import tmux as _tmux
55
+ from .resources import tunnel as _tunnel
56
+ from .resources import tunnel_presets as _tunnel_presets
57
+ from .resources import user_admin as _user_admin
58
+ from .resources import users as _users
59
+ from .resources import vault as _vault
60
+ from .resources import webauthn as _webauthn
61
+ from .resources import workspaces as _workspaces
62
+
63
+ # --- generated resource imports end ---
64
+
65
+ if TYPE_CHECKING:
66
+ from ._auth import AsyncPendingTOTP
67
+
68
+
69
+ class AsyncTermixClient:
70
+ """
71
+ Example:
72
+ >>> client = AsyncTermixClient(base_url="https://termix.example.com", api_key="tmx_...")
73
+ >>> await client.request("GET", "/health")
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ *,
79
+ base_url: str,
80
+ api_key: str | None = None,
81
+ jwt: str | None = None,
82
+ service_urls: dict[str, str] | None = None,
83
+ timeout: float = 30.0,
84
+ verify: bool = True,
85
+ max_network_retries: int = 2,
86
+ default_headers: dict[str, str] | None = None,
87
+ _http_client: AsyncHTTPClient | None = None,
88
+ ) -> None:
89
+ self._options = ClientOptions(
90
+ base_url=base_url,
91
+ service_urls=service_urls,
92
+ api_key=api_key,
93
+ jwt=jwt,
94
+ timeout=timeout,
95
+ verify=verify,
96
+ max_network_retries=max_network_retries,
97
+ default_headers=default_headers,
98
+ )
99
+ self._requestor = AsyncAPIRequestor(self._options, http_client=_http_client)
100
+
101
+ # --- generated resource attributes start ---
102
+
103
+ self.ai = _ai.AsyncAiService(self._requestor)
104
+ self.alerts = _alerts.AsyncAlertsService(self._requestor)
105
+ self.api_keys = _api_keys.AsyncApiKeysService(self._requestor)
106
+ self.audit = _audit.AsyncAuditService(self._requestor)
107
+ self.automations = _automations.AsyncAutomationsService(self._requestor)
108
+ self.credentials = _credentials.AsyncCredentialsService(self._requestor)
109
+ self.dashboard = _dashboard.AsyncDashboardService(self._requestor)
110
+ self.database = _database.AsyncDatabaseService(self._requestor)
111
+ self.docker = _docker.AsyncDockerService(self._requestor)
112
+ self.encryption = _encryption.AsyncEncryptionService(self._requestor)
113
+ self.file_manager = _file_manager.AsyncFileManagerService(self._requestor)
114
+ self.fleets = _fleets.AsyncFleetsService(self._requestor)
115
+ self.guacamole = _guacamole.AsyncGuacamoleService(self._requestor)
116
+ self.homepage = _homepage.AsyncHomepageService(self._requestor)
117
+ self.host_file_manager = _host_file_manager.AsyncHostFileManagerService(self._requestor)
118
+ self.hosts = _hosts.AsyncHostsService(self._requestor)
119
+ self.instance_settings = _instance_settings.AsyncInstanceSettingsService(self._requestor)
120
+ self.metrics = _metrics.AsyncMetricsService(self._requestor)
121
+ self.network_topology = _network_topology.AsyncNetworkTopologyService(self._requestor)
122
+ self.open_tabs = _open_tabs.AsyncOpenTabsService(self._requestor)
123
+ self.preferences = _preferences.AsyncPreferencesService(self._requestor)
124
+ self.proxmox = _proxmox.AsyncProxmoxService(self._requestor)
125
+ self.proxmox_stats = _proxmox_stats.AsyncProxmoxStatsService(self._requestor)
126
+ self.rbac = _rbac.AsyncRbacService(self._requestor)
127
+ self.session_logs = _session_logs.AsyncSessionLogsService(self._requestor)
128
+ self.session_sharing = _session_sharing.AsyncSessionSharingService(self._requestor)
129
+ self.snippets = _snippets.AsyncSnippetsService(self._requestor)
130
+ self.sso = _sso.AsyncSsoService(self._requestor)
131
+ self.sync = _sync.AsyncSyncService(self._requestor)
132
+ self.system = _system.AsyncSystemService(self._requestor)
133
+ self.tailscale = _tailscale.AsyncTailscaleService(self._requestor)
134
+ self.terminal = _terminal.AsyncTerminalService(self._requestor)
135
+ self.termix_id = _termix_id.AsyncTermixIdService(self._requestor)
136
+ self.tmux = _tmux.AsyncTmuxService(self._requestor)
137
+ self.tunnel = _tunnel.AsyncTunnelService(self._requestor)
138
+ self.tunnel_presets = _tunnel_presets.AsyncTunnelPresetsService(self._requestor)
139
+ self.user_admin = _user_admin.AsyncUserAdminService(self._requestor)
140
+ self.users = _users.AsyncUsersService(self._requestor)
141
+ self.vault = _vault.AsyncVaultService(self._requestor)
142
+ self.webauthn = _webauthn.AsyncWebauthnService(self._requestor)
143
+ self.workspaces = _workspaces.AsyncWorkspacesService(self._requestor)
144
+
145
+ # --- generated resource attributes end ---
146
+
147
+ @classmethod
148
+ def _with_options(
149
+ cls, options: ClientOptions, requestor: AsyncAPIRequestor
150
+ ) -> AsyncTermixClient:
151
+ instance = cls.__new__(cls)
152
+ instance._options = options
153
+ instance._requestor = requestor
154
+ return instance
155
+
156
+ @classmethod
157
+ async def login(
158
+ cls,
159
+ *,
160
+ base_url: str,
161
+ username: str,
162
+ password: str,
163
+ remember_me: bool = False,
164
+ service_urls: dict[str, str] | None = None,
165
+ timeout: float = 30.0,
166
+ verify: bool = True,
167
+ max_network_retries: int = 2,
168
+ default_headers: dict[str, str] | None = None,
169
+ _http_client: AsyncHTTPClient | None = None,
170
+ ) -> AsyncTermixClient | AsyncPendingTOTP:
171
+ """Async mirror of `TermixClient.login()` — see its docstring and
172
+ `_auth.py`.
173
+ """
174
+ from ._auth import login_async
175
+
176
+ return await login_async(
177
+ base_url=base_url,
178
+ username=username,
179
+ password=password,
180
+ remember_me=remember_me,
181
+ service_urls=service_urls,
182
+ timeout=timeout,
183
+ verify=verify,
184
+ max_network_retries=max_network_retries,
185
+ default_headers=default_headers,
186
+ _http_client=_http_client,
187
+ )
188
+
189
+ async def request(
190
+ self,
191
+ method: str,
192
+ path: str,
193
+ *,
194
+ service: str | None = None,
195
+ query: Mapping[str, Any] | None = None,
196
+ json_body: Any | None = None,
197
+ files: Mapping[str, Any] | None = None,
198
+ options: RequestOptions | None = None,
199
+ ) -> TermixResponse | None:
200
+ return await self._requestor.request(
201
+ method,
202
+ path,
203
+ service=service,
204
+ query=query,
205
+ json_body=json_body,
206
+ files=files,
207
+ options=options,
208
+ )
209
+
210
+ async def close(self) -> None:
211
+ await self._requestor.close()
212
+
213
+ async def __aenter__(self) -> AsyncTermixClient:
214
+ return self
215
+
216
+ async def __aexit__(self, exc_type, exc, tb) -> None:
217
+ await self.close()