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.
- termix_sdk/__init__.py +57 -0
- termix_sdk/_api_requestor.py +357 -0
- termix_sdk/_async_client.py +217 -0
- termix_sdk/_auth.py +225 -0
- termix_sdk/_client.py +237 -0
- termix_sdk/_client_options.py +114 -0
- termix_sdk/_error.py +156 -0
- termix_sdk/_http_client.py +387 -0
- termix_sdk/_object.py +181 -0
- termix_sdk/_request_options.py +44 -0
- termix_sdk/_response.py +106 -0
- termix_sdk/_service.py +153 -0
- termix_sdk/_session.py +214 -0
- termix_sdk/_sse.py +79 -0
- termix_sdk/_util.py +93 -0
- termix_sdk/_version.py +6 -0
- termix_sdk/models/__init__.py +3 -0
- termix_sdk/models/ai.py +59 -0
- termix_sdk/models/alerts.py +115 -0
- termix_sdk/models/api_keys.py +29 -0
- termix_sdk/models/audit.py +21 -0
- termix_sdk/models/automations.py +79 -0
- termix_sdk/models/credentials.py +102 -0
- termix_sdk/models/dashboard.py +36 -0
- termix_sdk/models/database.py +38 -0
- termix_sdk/models/docker.py +106 -0
- termix_sdk/models/encryption.py +35 -0
- termix_sdk/models/file_manager.py +234 -0
- termix_sdk/models/fleets.py +75 -0
- termix_sdk/models/guacamole.py +24 -0
- termix_sdk/models/homepage.py +46 -0
- termix_sdk/models/host_file_manager.py +34 -0
- termix_sdk/models/hosts.py +608 -0
- termix_sdk/models/instance_settings.py +215 -0
- termix_sdk/models/metrics.py +253 -0
- termix_sdk/models/network_topology.py +14 -0
- termix_sdk/models/open_tabs.py +43 -0
- termix_sdk/models/preferences.py +105 -0
- termix_sdk/models/proxmox.py +11 -0
- termix_sdk/models/proxmox_stats.py +37 -0
- termix_sdk/models/rbac.py +105 -0
- termix_sdk/models/session_logs.py +30 -0
- termix_sdk/models/session_sharing.py +34 -0
- termix_sdk/models/snippets.py +84 -0
- termix_sdk/models/sso.py +31 -0
- termix_sdk/models/sync.py +27 -0
- termix_sdk/models/system.py +22 -0
- termix_sdk/models/tailscale.py +15 -0
- termix_sdk/models/terminal.py +27 -0
- termix_sdk/models/termix_id.py +91 -0
- termix_sdk/models/tmux.py +15 -0
- termix_sdk/models/tunnel.py +36 -0
- termix_sdk/models/tunnel_presets.py +22 -0
- termix_sdk/models/user_admin.py +60 -0
- termix_sdk/models/users.py +142 -0
- termix_sdk/models/vault.py +26 -0
- termix_sdk/models/webauthn.py +32 -0
- termix_sdk/models/workspaces.py +95 -0
- termix_sdk/py.typed +0 -0
- termix_sdk/resources/__init__.py +3 -0
- termix_sdk/resources/ai.py +457 -0
- termix_sdk/resources/alerts.py +562 -0
- termix_sdk/resources/api_keys.py +113 -0
- termix_sdk/resources/audit.py +120 -0
- termix_sdk/resources/automations.py +332 -0
- termix_sdk/resources/credentials.py +648 -0
- termix_sdk/resources/dashboard.py +287 -0
- termix_sdk/resources/database.py +214 -0
- termix_sdk/resources/docker.py +689 -0
- termix_sdk/resources/encryption.py +145 -0
- termix_sdk/resources/file_manager.py +1468 -0
- termix_sdk/resources/fleets.py +535 -0
- termix_sdk/resources/guacamole.py +133 -0
- termix_sdk/resources/homepage.py +364 -0
- termix_sdk/resources/host_file_manager.py +331 -0
- termix_sdk/resources/hosts.py +1047 -0
- termix_sdk/resources/instance_settings.py +1381 -0
- termix_sdk/resources/metrics.py +1917 -0
- termix_sdk/resources/network_topology.py +82 -0
- termix_sdk/resources/open_tabs.py +233 -0
- termix_sdk/resources/preferences.py +376 -0
- termix_sdk/resources/proxmox.py +117 -0
- termix_sdk/resources/proxmox_stats.py +279 -0
- termix_sdk/resources/rbac.py +762 -0
- termix_sdk/resources/session_logs.py +208 -0
- termix_sdk/resources/session_sharing.py +197 -0
- termix_sdk/resources/snippets.py +495 -0
- termix_sdk/resources/sso.py +223 -0
- termix_sdk/resources/sync.py +183 -0
- termix_sdk/resources/system.py +116 -0
- termix_sdk/resources/tailscale.py +50 -0
- termix_sdk/resources/terminal.py +278 -0
- termix_sdk/resources/termix_id.py +620 -0
- termix_sdk/resources/tmux.py +516 -0
- termix_sdk/resources/tunnel.py +217 -0
- termix_sdk/resources/tunnel_presets.py +152 -0
- termix_sdk/resources/user_admin.py +356 -0
- termix_sdk/resources/users.py +884 -0
- termix_sdk/resources/vault.py +155 -0
- termix_sdk/resources/webauthn.py +269 -0
- termix_sdk/resources/workspaces.py +403 -0
- termix_sdk/types/__init__.py +3 -0
- termix_sdk/types/ai.py +41 -0
- termix_sdk/types/alerts.py +56 -0
- termix_sdk/types/api_keys.py +16 -0
- termix_sdk/types/audit.py +19 -0
- termix_sdk/types/automations.py +38 -0
- termix_sdk/types/credentials.py +86 -0
- termix_sdk/types/dashboard.py +39 -0
- termix_sdk/types/database.py +20 -0
- termix_sdk/types/docker.py +60 -0
- termix_sdk/types/encryption.py +11 -0
- termix_sdk/types/file_manager.py +181 -0
- termix_sdk/types/fleets.py +53 -0
- termix_sdk/types/guacamole.py +27 -0
- termix_sdk/types/homepage.py +46 -0
- termix_sdk/types/host_file_manager.py +40 -0
- termix_sdk/types/hosts.py +355 -0
- termix_sdk/types/instance_settings.py +114 -0
- termix_sdk/types/metrics.py +130 -0
- termix_sdk/types/network_topology.py +14 -0
- termix_sdk/types/open_tabs.py +30 -0
- termix_sdk/types/preferences.py +68 -0
- termix_sdk/types/proxmox.py +18 -0
- termix_sdk/types/proxmox_stats.py +27 -0
- termix_sdk/types/rbac.py +53 -0
- termix_sdk/types/session_logs.py +14 -0
- termix_sdk/types/session_sharing.py +21 -0
- termix_sdk/types/snippets.py +52 -0
- termix_sdk/types/sso.py +33 -0
- termix_sdk/types/sync.py +27 -0
- termix_sdk/types/system.py +19 -0
- termix_sdk/types/tailscale.py +11 -0
- termix_sdk/types/terminal.py +31 -0
- termix_sdk/types/termix_id.py +51 -0
- termix_sdk/types/tmux.py +54 -0
- termix_sdk/types/tunnel.py +38 -0
- termix_sdk/types/tunnel_presets.py +24 -0
- termix_sdk/types/user_admin.py +36 -0
- termix_sdk/types/users.py +81 -0
- termix_sdk/types/vault.py +42 -0
- termix_sdk/types/webauthn.py +31 -0
- termix_sdk/types/workspaces.py +35 -0
- termix_sdk-0.1.0.dist-info/METADATA +191 -0
- termix_sdk-0.1.0.dist-info/RECORD +148 -0
- termix_sdk-0.1.0.dist-info/WHEEL +4 -0
- termix_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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()
|