kaitencloud 1.0.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.
- kaitencloud/__init__.py +95 -0
- kaitencloud/_async/__init__.py +4 -0
- kaitencloud/_async/_http.py +327 -0
- kaitencloud/_async/client.py +310 -0
- kaitencloud/_async/resources/__init__.py +34 -0
- kaitencloud/_async/resources/_base.py +20 -0
- kaitencloud/_async/resources/components.py +76 -0
- kaitencloud/_async/resources/connectors.py +92 -0
- kaitencloud/_async/resources/customers.py +144 -0
- kaitencloud/_async/resources/deployment_zones.py +95 -0
- kaitencloud/_async/resources/entitlement_groups.py +89 -0
- kaitencloud/_async/resources/entitlements.py +202 -0
- kaitencloud/_async/resources/feature_flags.py +164 -0
- kaitencloud/_async/resources/instances.py +349 -0
- kaitencloud/_async/resources/integrations.py +143 -0
- kaitencloud/_async/resources/license_families.py +61 -0
- kaitencloud/_async/resources/licenses.py +247 -0
- kaitencloud/_async/resources/metadata_fields.py +115 -0
- kaitencloud/_async/resources/platform/__init__.py +14 -0
- kaitencloud/_async/resources/platform/connectors.py +53 -0
- kaitencloud/_async/resources/platform/organizations.py +68 -0
- kaitencloud/_async/resources/platform/tokens.py +92 -0
- kaitencloud/_async/resources/platform/users.py +26 -0
- kaitencloud/_async/resources/releases.py +59 -0
- kaitencloud/_async/resources/service_accounts.py +93 -0
- kaitencloud/_constants.py +32 -0
- kaitencloud/_exceptions.py +254 -0
- kaitencloud/_models.py +39 -0
- kaitencloud/_sync/__init__.py +6 -0
- kaitencloud/_sync/_http.py +329 -0
- kaitencloud/_sync/client.py +312 -0
- kaitencloud/_sync/resources/__init__.py +36 -0
- kaitencloud/_sync/resources/_base.py +22 -0
- kaitencloud/_sync/resources/components.py +76 -0
- kaitencloud/_sync/resources/connectors.py +92 -0
- kaitencloud/_sync/resources/customers.py +144 -0
- kaitencloud/_sync/resources/deployment_zones.py +93 -0
- kaitencloud/_sync/resources/entitlement_groups.py +91 -0
- kaitencloud/_sync/resources/entitlements.py +204 -0
- kaitencloud/_sync/resources/feature_flags.py +166 -0
- kaitencloud/_sync/resources/instances.py +349 -0
- kaitencloud/_sync/resources/integrations.py +145 -0
- kaitencloud/_sync/resources/license_families.py +63 -0
- kaitencloud/_sync/resources/licenses.py +245 -0
- kaitencloud/_sync/resources/metadata_fields.py +117 -0
- kaitencloud/_sync/resources/platform/__init__.py +16 -0
- kaitencloud/_sync/resources/platform/connectors.py +53 -0
- kaitencloud/_sync/resources/platform/organizations.py +70 -0
- kaitencloud/_sync/resources/platform/tokens.py +94 -0
- kaitencloud/_sync/resources/platform/users.py +28 -0
- kaitencloud/_sync/resources/releases.py +61 -0
- kaitencloud/_sync/resources/service_accounts.py +93 -0
- kaitencloud/_types.py +56 -0
- kaitencloud/_utils.py +230 -0
- kaitencloud/_version.py +4 -0
- kaitencloud/py.typed +0 -0
- kaitencloud/targeting.py +108 -0
- kaitencloud/types/__init__.py +197 -0
- kaitencloud/types/_core.py +1582 -0
- kaitencloud/types/_params.py +126 -0
- kaitencloud/types/_platform.py +62 -0
- kaitencloud/types/_webhooks.py +1043 -0
- kaitencloud/usage.py +94 -0
- kaitencloud/webhooks.py +189 -0
- kaitencloud-1.0.0.dist-info/METADATA +845 -0
- kaitencloud-1.0.0.dist-info/RECORD +69 -0
- kaitencloud-1.0.0.dist-info/WHEEL +4 -0
- kaitencloud-1.0.0.dist-info/licenses/LICENSE +202 -0
- kaitencloud-1.0.0.dist-info/licenses/NOTICE +3 -0
kaitencloud/__init__.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Copyright 2026 KAITEN INC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""The official Python SDK for Kaiten.
|
|
5
|
+
|
|
6
|
+
::
|
|
7
|
+
|
|
8
|
+
from kaitencloud import KaitenClient
|
|
9
|
+
|
|
10
|
+
client = KaitenClient(token="ksh_...")
|
|
11
|
+
for instance in client.instances.list():
|
|
12
|
+
print(instance.slug, instance.status)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from . import targeting, types, usage
|
|
21
|
+
from ._async.client import AsyncKaitenClient, AsyncKaitenPlatformClient
|
|
22
|
+
from ._constants import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
|
|
23
|
+
from ._exceptions import (
|
|
24
|
+
APIConnectionError,
|
|
25
|
+
APIResponseValidationError,
|
|
26
|
+
APIStatusError,
|
|
27
|
+
APITimeoutError,
|
|
28
|
+
AuthenticationError,
|
|
29
|
+
BadRequestError,
|
|
30
|
+
ConflictError,
|
|
31
|
+
CredentialError,
|
|
32
|
+
InternalServerError,
|
|
33
|
+
KaitenError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
PaginationError,
|
|
36
|
+
PermissionDeniedError,
|
|
37
|
+
RateLimitError,
|
|
38
|
+
ServiceUnavailableError,
|
|
39
|
+
ThresholdExceededError,
|
|
40
|
+
UnprocessableEntityError,
|
|
41
|
+
WebhookPayloadError,
|
|
42
|
+
WebhookVerificationError,
|
|
43
|
+
)
|
|
44
|
+
from ._models import KaitenModel
|
|
45
|
+
from ._sync.client import KaitenClient, KaitenPlatformClient
|
|
46
|
+
from ._types import NOT_GIVEN, NotGiven
|
|
47
|
+
from ._version import __version__
|
|
48
|
+
from .usage import UNLIMITED
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from . import webhooks
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"DEFAULT_MAX_RETRIES",
|
|
55
|
+
"DEFAULT_TIMEOUT",
|
|
56
|
+
"NOT_GIVEN",
|
|
57
|
+
"UNLIMITED",
|
|
58
|
+
"APIConnectionError",
|
|
59
|
+
"APIResponseValidationError",
|
|
60
|
+
"APIStatusError",
|
|
61
|
+
"APITimeoutError",
|
|
62
|
+
"AsyncKaitenClient",
|
|
63
|
+
"AsyncKaitenPlatformClient",
|
|
64
|
+
"AuthenticationError",
|
|
65
|
+
"BadRequestError",
|
|
66
|
+
"ConflictError",
|
|
67
|
+
"CredentialError",
|
|
68
|
+
"InternalServerError",
|
|
69
|
+
"KaitenClient",
|
|
70
|
+
"KaitenError",
|
|
71
|
+
"KaitenModel",
|
|
72
|
+
"KaitenPlatformClient",
|
|
73
|
+
"NotFoundError",
|
|
74
|
+
"NotGiven",
|
|
75
|
+
"PaginationError",
|
|
76
|
+
"PermissionDeniedError",
|
|
77
|
+
"RateLimitError",
|
|
78
|
+
"ServiceUnavailableError",
|
|
79
|
+
"ThresholdExceededError",
|
|
80
|
+
"UnprocessableEntityError",
|
|
81
|
+
"WebhookPayloadError",
|
|
82
|
+
"WebhookVerificationError",
|
|
83
|
+
"__version__",
|
|
84
|
+
"targeting",
|
|
85
|
+
"types",
|
|
86
|
+
"usage",
|
|
87
|
+
"webhooks",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def __getattr__(name: str) -> Any:
|
|
92
|
+
# The webhook event models are only loaded by the code that receives webhooks.
|
|
93
|
+
if name == "webhooks":
|
|
94
|
+
return importlib.import_module(f"{__name__}.webhooks")
|
|
95
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Copyright 2026 KAITEN INC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""The transport both clients share: authentication, retries, errors and pagination."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from collections.abc import AsyncIterator, Mapping
|
|
10
|
+
from typing import Any, TypeVar
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from pydantic import BaseModel, ValidationError
|
|
14
|
+
|
|
15
|
+
from .._constants import DEFAULT_TIMEOUT, MAX_PAGE_SIZE
|
|
16
|
+
from .._exceptions import (
|
|
17
|
+
APIConnectionError,
|
|
18
|
+
APIResponseValidationError,
|
|
19
|
+
APIStatusError,
|
|
20
|
+
APITimeoutError,
|
|
21
|
+
PaginationError,
|
|
22
|
+
make_status_error,
|
|
23
|
+
)
|
|
24
|
+
from .._types import NOT_GIVEN, AsyncCredential, NotGiven, TimeoutTypes
|
|
25
|
+
from .._utils import USER_AGENT, Surface, async_resolve_token, async_sleep, check_token, retry_delay
|
|
26
|
+
|
|
27
|
+
__all__ = ["AsyncAPIClient"]
|
|
28
|
+
|
|
29
|
+
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("kaitencloud")
|
|
32
|
+
|
|
33
|
+
# Methods a retry cannot turn into a second effect. POST and PATCH are not among them: a usage
|
|
34
|
+
# report retried after a lost response is usage counted twice, and usage is what is billed.
|
|
35
|
+
IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "PUT", "DELETE"})
|
|
36
|
+
|
|
37
|
+
# Failures that happen before a request is sent, and so are safe to retry whatever its method.
|
|
38
|
+
NEVER_SENT = (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AsyncAPIClient:
|
|
42
|
+
"""Sends a client's requests: authenticates, retries what is safe, maps errors, walks pages."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
base_url: str,
|
|
48
|
+
credential: AsyncCredential,
|
|
49
|
+
surface: Surface,
|
|
50
|
+
max_retries: int,
|
|
51
|
+
timeout: TimeoutTypes | NotGiven = NOT_GIVEN,
|
|
52
|
+
default_headers: Mapping[str, str] | None = None,
|
|
53
|
+
http_client: httpx.AsyncClient | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
if max_retries < 0:
|
|
56
|
+
raise ValueError(f"max_retries must be zero or more, got {max_retries}")
|
|
57
|
+
self.base_url = base_url
|
|
58
|
+
self.credential = credential
|
|
59
|
+
self.surface = surface
|
|
60
|
+
self.max_retries = max_retries
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
self.default_headers = dict(default_headers or {})
|
|
63
|
+
self.owns_http_client = http_client is None
|
|
64
|
+
self.http_client = http_client or httpx.AsyncClient(
|
|
65
|
+
timeout=DEFAULT_TIMEOUT if isinstance(timeout, NotGiven) else timeout
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def copy(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
timeout: TimeoutTypes | NotGiven = NOT_GIVEN,
|
|
72
|
+
max_retries: int | NotGiven = NOT_GIVEN,
|
|
73
|
+
default_headers: Mapping[str, str] | NotGiven = NOT_GIVEN,
|
|
74
|
+
) -> AsyncAPIClient:
|
|
75
|
+
"""A transport with some options changed, sharing this one's connection pool."""
|
|
76
|
+
return AsyncAPIClient(
|
|
77
|
+
base_url=self.base_url,
|
|
78
|
+
credential=self.credential,
|
|
79
|
+
surface=self.surface,
|
|
80
|
+
max_retries=self.max_retries if isinstance(max_retries, NotGiven) else max_retries,
|
|
81
|
+
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
|
|
82
|
+
default_headers=(
|
|
83
|
+
self.default_headers if isinstance(default_headers, NotGiven) else default_headers
|
|
84
|
+
),
|
|
85
|
+
http_client=self.http_client,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def close(self) -> None:
|
|
89
|
+
if self.owns_http_client:
|
|
90
|
+
await self.http_client.aclose()
|
|
91
|
+
|
|
92
|
+
async def request(
|
|
93
|
+
self,
|
|
94
|
+
method: str,
|
|
95
|
+
path: str,
|
|
96
|
+
*,
|
|
97
|
+
params: Mapping[str, Any] | None = None,
|
|
98
|
+
body: Any = None,
|
|
99
|
+
idempotent: bool | None = None,
|
|
100
|
+
errors: Mapping[int, type[APIStatusError]] | None = None,
|
|
101
|
+
) -> httpx.Response:
|
|
102
|
+
"""Send one operation and return its successful response.
|
|
103
|
+
|
|
104
|
+
A request is retried, up to ``max_retries`` times, when it failed in a way worth
|
|
105
|
+
retrying (a connection error, a 408, 429 or 5xx) *and* repeating it is safe: its method
|
|
106
|
+
is idempotent, ``idempotent=True`` says so, or it never left the machine. A 401 or 403 is
|
|
107
|
+
never retried -- the SDK cannot mint a better credential, and hammering a rejected one
|
|
108
|
+
only burns the rate budget.
|
|
109
|
+
"""
|
|
110
|
+
url = self.base_url + path.lstrip("/")
|
|
111
|
+
retryable = method in IDEMPOTENT_METHODS if idempotent is None else idempotent
|
|
112
|
+
attempt = 0
|
|
113
|
+
|
|
114
|
+
while True:
|
|
115
|
+
request = self._build(
|
|
116
|
+
method, url, params=_query(params), body=body, headers=await self._headers()
|
|
117
|
+
)
|
|
118
|
+
try:
|
|
119
|
+
response = await self.http_client.send(request)
|
|
120
|
+
except httpx.TransportError as error:
|
|
121
|
+
if attempt < self.max_retries and (retryable or isinstance(error, NEVER_SENT)):
|
|
122
|
+
await self._back_off(request, attempt, repr(error))
|
|
123
|
+
attempt += 1
|
|
124
|
+
continue
|
|
125
|
+
if isinstance(error, httpx.TimeoutException):
|
|
126
|
+
raise APITimeoutError(request=request) from error
|
|
127
|
+
raise APIConnectionError(request=request) from error
|
|
128
|
+
|
|
129
|
+
if response.is_success:
|
|
130
|
+
return response
|
|
131
|
+
|
|
132
|
+
status = response.status_code
|
|
133
|
+
if retryable and attempt < self.max_retries and (status in (408, 429) or status >= 500):
|
|
134
|
+
await self._back_off(
|
|
135
|
+
request, attempt, f"HTTP {status}", response.headers.get("retry-after")
|
|
136
|
+
)
|
|
137
|
+
attempt += 1
|
|
138
|
+
continue
|
|
139
|
+
raise make_status_error(response, overrides=errors)
|
|
140
|
+
|
|
141
|
+
async def get(
|
|
142
|
+
self, path: str, model: type[ModelT], *, params: Mapping[str, Any] | None = None
|
|
143
|
+
) -> ModelT:
|
|
144
|
+
return await self.send("GET", path, model, params=params)
|
|
145
|
+
|
|
146
|
+
async def get_list(
|
|
147
|
+
self, path: str, model: type[ModelT], *, params: Mapping[str, Any] | None = None
|
|
148
|
+
) -> list[ModelT]:
|
|
149
|
+
"""GET an unpaginated array, which the contracts type as nullable: null is empty."""
|
|
150
|
+
response = await self.request("GET", path, params=params)
|
|
151
|
+
data = self.decode(response)
|
|
152
|
+
if data is None:
|
|
153
|
+
return []
|
|
154
|
+
if not isinstance(data, list):
|
|
155
|
+
raise APIResponseValidationError(
|
|
156
|
+
f"GET {path}: expected a JSON array, got {type(data).__name__}", response=response
|
|
157
|
+
)
|
|
158
|
+
return [self.validate(model, item, response) for item in data]
|
|
159
|
+
|
|
160
|
+
async def send(
|
|
161
|
+
self,
|
|
162
|
+
method: str,
|
|
163
|
+
path: str,
|
|
164
|
+
model: type[ModelT],
|
|
165
|
+
*,
|
|
166
|
+
params: Mapping[str, Any] | None = None,
|
|
167
|
+
body: Any = None,
|
|
168
|
+
idempotent: bool | None = None,
|
|
169
|
+
errors: Mapping[int, type[APIStatusError]] | None = None,
|
|
170
|
+
) -> ModelT:
|
|
171
|
+
response = await self.request(
|
|
172
|
+
method, path, params=params, body=body, idempotent=idempotent, errors=errors
|
|
173
|
+
)
|
|
174
|
+
return self.validate(model, self.decode(response), response)
|
|
175
|
+
|
|
176
|
+
async def send_empty(
|
|
177
|
+
self,
|
|
178
|
+
method: str,
|
|
179
|
+
path: str,
|
|
180
|
+
*,
|
|
181
|
+
params: Mapping[str, Any] | None = None,
|
|
182
|
+
body: Any = None,
|
|
183
|
+
idempotent: bool | None = None,
|
|
184
|
+
) -> None:
|
|
185
|
+
await self.request(method, path, params=params, body=body, idempotent=idempotent)
|
|
186
|
+
|
|
187
|
+
async def paginate(
|
|
188
|
+
self,
|
|
189
|
+
path: str,
|
|
190
|
+
model: type[ModelT],
|
|
191
|
+
*,
|
|
192
|
+
params: Mapping[str, Any] | None = None,
|
|
193
|
+
limit: int | None = None,
|
|
194
|
+
) -> AsyncIterator[ModelT]:
|
|
195
|
+
"""Yield every row of a cursor-paginated collection, page after page.
|
|
196
|
+
|
|
197
|
+
``limit`` caps how many rows are yielded, and no page asks for more rows than are still
|
|
198
|
+
wanted. Without it, the walk ends only when the API says there is nothing more.
|
|
199
|
+
"""
|
|
200
|
+
filters = dict(params or {})
|
|
201
|
+
cursor: str | None = None
|
|
202
|
+
yielded = 0
|
|
203
|
+
items: list[Any]
|
|
204
|
+
has_more: object
|
|
205
|
+
next_cursor: object
|
|
206
|
+
while True:
|
|
207
|
+
size = MAX_PAGE_SIZE if limit is None else min(MAX_PAGE_SIZE, limit - yielded)
|
|
208
|
+
response = await self.request(
|
|
209
|
+
"GET", path, params={**filters, "limit": size, "cursor": cursor}
|
|
210
|
+
)
|
|
211
|
+
page = self.decode(response)
|
|
212
|
+
|
|
213
|
+
if isinstance(page, list):
|
|
214
|
+
# A deployment older than cursor pagination answers the whole collection.
|
|
215
|
+
items, has_more, next_cursor = page, False, None
|
|
216
|
+
elif isinstance(page, dict) and isinstance(page.get("items"), list):
|
|
217
|
+
items, has_more, next_cursor = (
|
|
218
|
+
page["items"],
|
|
219
|
+
page.get("hasMore"),
|
|
220
|
+
page.get("nextCursor"),
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
raise APIResponseValidationError(
|
|
224
|
+
f"GET {path}: expected a page of items", response=response
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
for item in items:
|
|
228
|
+
yield self.validate(model, item, response)
|
|
229
|
+
yielded += 1
|
|
230
|
+
if limit is not None and yielded >= limit:
|
|
231
|
+
return
|
|
232
|
+
if not has_more:
|
|
233
|
+
return
|
|
234
|
+
if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
|
|
235
|
+
raise PaginationError(
|
|
236
|
+
f"GET {path}: the API reported more rows but returned no usable next cursor"
|
|
237
|
+
)
|
|
238
|
+
cursor = next_cursor
|
|
239
|
+
|
|
240
|
+
async def list_all(
|
|
241
|
+
self,
|
|
242
|
+
path: str,
|
|
243
|
+
model: type[ModelT],
|
|
244
|
+
*,
|
|
245
|
+
params: Mapping[str, Any] | None = None,
|
|
246
|
+
limit: int | None = None,
|
|
247
|
+
) -> list[ModelT]:
|
|
248
|
+
rows: list[ModelT] = []
|
|
249
|
+
async for row in self.paginate(path, model, params=params, limit=limit):
|
|
250
|
+
rows.append(row)
|
|
251
|
+
return rows
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def decode(response: httpx.Response) -> Any:
|
|
255
|
+
if not response.content:
|
|
256
|
+
return None
|
|
257
|
+
try:
|
|
258
|
+
return response.json()
|
|
259
|
+
except ValueError as error:
|
|
260
|
+
raise APIResponseValidationError(
|
|
261
|
+
f"{response.request.method} {response.request.url.path}: the response is not JSON",
|
|
262
|
+
response=response,
|
|
263
|
+
) from error
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def validate(model: type[ModelT], data: Any, response: httpx.Response) -> ModelT:
|
|
267
|
+
try:
|
|
268
|
+
return model.model_validate(data)
|
|
269
|
+
except ValidationError as error:
|
|
270
|
+
raise APIResponseValidationError(
|
|
271
|
+
f"{response.request.method} {response.request.url.path}: the response does not "
|
|
272
|
+
f"match {model.__name__}: {error}",
|
|
273
|
+
response=response,
|
|
274
|
+
) from error
|
|
275
|
+
|
|
276
|
+
def _build(
|
|
277
|
+
self,
|
|
278
|
+
method: str,
|
|
279
|
+
url: str,
|
|
280
|
+
*,
|
|
281
|
+
params: dict[str, Any] | None,
|
|
282
|
+
body: Any,
|
|
283
|
+
headers: dict[str, str],
|
|
284
|
+
) -> httpx.Request:
|
|
285
|
+
if isinstance(self.timeout, NotGiven):
|
|
286
|
+
# The HTTP client's own timeout applies: the default one, or the caller's.
|
|
287
|
+
return self.http_client.build_request(
|
|
288
|
+
method, url, params=params, json=body, headers=headers
|
|
289
|
+
)
|
|
290
|
+
return self.http_client.build_request(
|
|
291
|
+
method, url, params=params, json=body, headers=headers, timeout=self.timeout
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
async def _headers(self) -> dict[str, str]:
|
|
295
|
+
token = check_token(await async_resolve_token(self.credential), self.surface)
|
|
296
|
+
return {
|
|
297
|
+
"Accept": "application/json",
|
|
298
|
+
"User-Agent": USER_AGENT,
|
|
299
|
+
**self.default_headers,
|
|
300
|
+
"Authorization": f"Bearer {token}",
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async def _back_off(
|
|
304
|
+
self, request: httpx.Request, attempt: int, reason: str, retry_after: str | None = None
|
|
305
|
+
) -> None:
|
|
306
|
+
delay = retry_delay(attempt, retry_after)
|
|
307
|
+
log.info(
|
|
308
|
+
"Retrying %s %s in %.2fs (retry %d of %d) after %s",
|
|
309
|
+
request.method,
|
|
310
|
+
request.url.path,
|
|
311
|
+
delay,
|
|
312
|
+
attempt + 1,
|
|
313
|
+
self.max_retries,
|
|
314
|
+
reason,
|
|
315
|
+
)
|
|
316
|
+
await async_sleep(delay)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _query(params: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
|
320
|
+
if not params:
|
|
321
|
+
return None
|
|
322
|
+
query: dict[str, Any] = {}
|
|
323
|
+
for key, value in params.items():
|
|
324
|
+
if value is None:
|
|
325
|
+
continue
|
|
326
|
+
query[key] = ("true" if value else "false") if isinstance(value, bool) else value
|
|
327
|
+
return query
|