kstlib 0.0.1a0__py3-none-any.whl → 1.0.1__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.
- kstlib/__init__.py +266 -1
- kstlib/__main__.py +16 -0
- kstlib/alerts/__init__.py +110 -0
- kstlib/alerts/channels/__init__.py +36 -0
- kstlib/alerts/channels/base.py +197 -0
- kstlib/alerts/channels/email.py +227 -0
- kstlib/alerts/channels/slack.py +389 -0
- kstlib/alerts/exceptions.py +72 -0
- kstlib/alerts/manager.py +651 -0
- kstlib/alerts/models.py +142 -0
- kstlib/alerts/throttle.py +263 -0
- kstlib/auth/__init__.py +139 -0
- kstlib/auth/callback.py +399 -0
- kstlib/auth/config.py +502 -0
- kstlib/auth/errors.py +127 -0
- kstlib/auth/models.py +316 -0
- kstlib/auth/providers/__init__.py +14 -0
- kstlib/auth/providers/base.py +393 -0
- kstlib/auth/providers/oauth2.py +645 -0
- kstlib/auth/providers/oidc.py +821 -0
- kstlib/auth/session.py +338 -0
- kstlib/auth/token.py +482 -0
- kstlib/cache/__init__.py +50 -0
- kstlib/cache/decorator.py +261 -0
- kstlib/cache/strategies.py +516 -0
- kstlib/cli/__init__.py +8 -0
- kstlib/cli/app.py +195 -0
- kstlib/cli/commands/__init__.py +5 -0
- kstlib/cli/commands/auth/__init__.py +39 -0
- kstlib/cli/commands/auth/common.py +122 -0
- kstlib/cli/commands/auth/login.py +325 -0
- kstlib/cli/commands/auth/logout.py +74 -0
- kstlib/cli/commands/auth/providers.py +57 -0
- kstlib/cli/commands/auth/status.py +291 -0
- kstlib/cli/commands/auth/token.py +199 -0
- kstlib/cli/commands/auth/whoami.py +106 -0
- kstlib/cli/commands/config.py +89 -0
- kstlib/cli/commands/ops/__init__.py +39 -0
- kstlib/cli/commands/ops/attach.py +49 -0
- kstlib/cli/commands/ops/common.py +269 -0
- kstlib/cli/commands/ops/list_sessions.py +252 -0
- kstlib/cli/commands/ops/logs.py +49 -0
- kstlib/cli/commands/ops/start.py +98 -0
- kstlib/cli/commands/ops/status.py +138 -0
- kstlib/cli/commands/ops/stop.py +60 -0
- kstlib/cli/commands/rapi/__init__.py +60 -0
- kstlib/cli/commands/rapi/call.py +341 -0
- kstlib/cli/commands/rapi/list.py +99 -0
- kstlib/cli/commands/rapi/show.py +206 -0
- kstlib/cli/commands/secrets/__init__.py +35 -0
- kstlib/cli/commands/secrets/common.py +425 -0
- kstlib/cli/commands/secrets/decrypt.py +88 -0
- kstlib/cli/commands/secrets/doctor.py +743 -0
- kstlib/cli/commands/secrets/encrypt.py +242 -0
- kstlib/cli/commands/secrets/shred.py +96 -0
- kstlib/cli/common.py +86 -0
- kstlib/config/__init__.py +76 -0
- kstlib/config/exceptions.py +110 -0
- kstlib/config/export.py +225 -0
- kstlib/config/loader.py +963 -0
- kstlib/config/sops.py +287 -0
- kstlib/db/__init__.py +54 -0
- kstlib/db/aiosqlcipher.py +137 -0
- kstlib/db/cipher.py +112 -0
- kstlib/db/database.py +367 -0
- kstlib/db/exceptions.py +25 -0
- kstlib/db/pool.py +302 -0
- kstlib/helpers/__init__.py +35 -0
- kstlib/helpers/exceptions.py +11 -0
- kstlib/helpers/time_trigger.py +396 -0
- kstlib/kstlib.conf.yml +890 -0
- kstlib/limits.py +963 -0
- kstlib/logging/__init__.py +108 -0
- kstlib/logging/manager.py +633 -0
- kstlib/mail/__init__.py +42 -0
- kstlib/mail/builder.py +626 -0
- kstlib/mail/exceptions.py +27 -0
- kstlib/mail/filesystem.py +248 -0
- kstlib/mail/transport.py +224 -0
- kstlib/mail/transports/__init__.py +19 -0
- kstlib/mail/transports/gmail.py +268 -0
- kstlib/mail/transports/resend.py +324 -0
- kstlib/mail/transports/smtp.py +326 -0
- kstlib/meta.py +72 -0
- kstlib/metrics/__init__.py +88 -0
- kstlib/metrics/decorators.py +1090 -0
- kstlib/metrics/exceptions.py +14 -0
- kstlib/monitoring/__init__.py +116 -0
- kstlib/monitoring/_styles.py +163 -0
- kstlib/monitoring/cell.py +57 -0
- kstlib/monitoring/config.py +424 -0
- kstlib/monitoring/delivery.py +579 -0
- kstlib/monitoring/exceptions.py +63 -0
- kstlib/monitoring/image.py +220 -0
- kstlib/monitoring/kv.py +79 -0
- kstlib/monitoring/list.py +69 -0
- kstlib/monitoring/metric.py +88 -0
- kstlib/monitoring/monitoring.py +341 -0
- kstlib/monitoring/renderer.py +139 -0
- kstlib/monitoring/service.py +392 -0
- kstlib/monitoring/table.py +129 -0
- kstlib/monitoring/types.py +56 -0
- kstlib/ops/__init__.py +86 -0
- kstlib/ops/base.py +148 -0
- kstlib/ops/container.py +577 -0
- kstlib/ops/exceptions.py +209 -0
- kstlib/ops/manager.py +407 -0
- kstlib/ops/models.py +176 -0
- kstlib/ops/tmux.py +372 -0
- kstlib/ops/validators.py +287 -0
- kstlib/py.typed +0 -0
- kstlib/rapi/__init__.py +118 -0
- kstlib/rapi/client.py +875 -0
- kstlib/rapi/config.py +861 -0
- kstlib/rapi/credentials.py +887 -0
- kstlib/rapi/exceptions.py +213 -0
- kstlib/resilience/__init__.py +101 -0
- kstlib/resilience/circuit_breaker.py +440 -0
- kstlib/resilience/exceptions.py +95 -0
- kstlib/resilience/heartbeat.py +491 -0
- kstlib/resilience/rate_limiter.py +506 -0
- kstlib/resilience/shutdown.py +417 -0
- kstlib/resilience/watchdog.py +637 -0
- kstlib/secrets/__init__.py +29 -0
- kstlib/secrets/exceptions.py +19 -0
- kstlib/secrets/models.py +62 -0
- kstlib/secrets/providers/__init__.py +79 -0
- kstlib/secrets/providers/base.py +58 -0
- kstlib/secrets/providers/environment.py +66 -0
- kstlib/secrets/providers/keyring.py +107 -0
- kstlib/secrets/providers/kms.py +223 -0
- kstlib/secrets/providers/kwargs.py +101 -0
- kstlib/secrets/providers/sops.py +209 -0
- kstlib/secrets/resolver.py +221 -0
- kstlib/secrets/sensitive.py +130 -0
- kstlib/secure/__init__.py +23 -0
- kstlib/secure/fs.py +194 -0
- kstlib/secure/permissions.py +70 -0
- kstlib/ssl.py +347 -0
- kstlib/ui/__init__.py +23 -0
- kstlib/ui/exceptions.py +26 -0
- kstlib/ui/panels.py +484 -0
- kstlib/ui/spinner.py +864 -0
- kstlib/ui/tables.py +382 -0
- kstlib/utils/__init__.py +48 -0
- kstlib/utils/dict.py +36 -0
- kstlib/utils/formatting.py +338 -0
- kstlib/utils/http_trace.py +237 -0
- kstlib/utils/lazy.py +49 -0
- kstlib/utils/secure_delete.py +205 -0
- kstlib/utils/serialization.py +247 -0
- kstlib/utils/text.py +56 -0
- kstlib/utils/validators.py +124 -0
- kstlib/websocket/__init__.py +97 -0
- kstlib/websocket/exceptions.py +214 -0
- kstlib/websocket/manager.py +1102 -0
- kstlib/websocket/models.py +361 -0
- kstlib-1.0.1.dist-info/METADATA +201 -0
- kstlib-1.0.1.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
- kstlib-1.0.1.dist-info/entry_points.txt +2 -0
- kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
- kstlib-0.0.1a0.dist-info/METADATA +0 -29
- kstlib-0.0.1a0.dist-info/RECORD +0 -6
- kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/top_level.txt +0 -0
kstlib/auth/session.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Authenticated HTTP session wrapper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
from kstlib.auth.errors import AuthError, TokenExpiredError
|
|
13
|
+
from kstlib.logging import TRACE_LEVEL, get_logger
|
|
14
|
+
from kstlib.ssl import build_ssl_context
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import types
|
|
18
|
+
|
|
19
|
+
from kstlib.auth.providers.base import AbstractAuthProvider
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
# Default timeout for HTTP requests
|
|
24
|
+
DEFAULT_TIMEOUT = 30.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuthSession:
|
|
28
|
+
"""HTTP session with automatic token injection and refresh.
|
|
29
|
+
|
|
30
|
+
Wraps httpx.Client (sync) or httpx.AsyncClient (async) to automatically:
|
|
31
|
+
- Inject Bearer token in Authorization header
|
|
32
|
+
- Refresh expired tokens before making requests
|
|
33
|
+
- Handle 401 responses by refreshing and retrying
|
|
34
|
+
|
|
35
|
+
Example (sync):
|
|
36
|
+
>>> from kstlib.auth import AuthSession, get_provider # doctest: +SKIP
|
|
37
|
+
>>> provider = get_provider("corporate") # doctest: +SKIP
|
|
38
|
+
>>> with AuthSession(provider) as session: # doctest: +SKIP
|
|
39
|
+
... response = session.get("https://api.example.com/users/me")
|
|
40
|
+
... print(response.json())
|
|
41
|
+
|
|
42
|
+
Example (async):
|
|
43
|
+
>>> async with AuthSession(provider) as session: # doctest: +SKIP
|
|
44
|
+
... response = await session.get("https://api.example.com/users/me")
|
|
45
|
+
... print(response.json())
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
provider: AbstractAuthProvider,
|
|
51
|
+
*,
|
|
52
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
53
|
+
auto_refresh: bool = True,
|
|
54
|
+
retry_on_401: bool = True,
|
|
55
|
+
ssl_verify: bool | None = None,
|
|
56
|
+
ssl_ca_bundle: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize authenticated session.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
provider: Authentication provider to use for tokens.
|
|
62
|
+
timeout: Default request timeout in seconds.
|
|
63
|
+
auto_refresh: Automatically refresh expired tokens before requests.
|
|
64
|
+
retry_on_401: Retry request after token refresh on 401 response.
|
|
65
|
+
ssl_verify: Override SSL verification (True/False).
|
|
66
|
+
If None, uses provider's SSL config or global config.
|
|
67
|
+
ssl_ca_bundle: Override CA bundle path.
|
|
68
|
+
If None, uses provider's SSL config or global config.
|
|
69
|
+
"""
|
|
70
|
+
self.provider = provider
|
|
71
|
+
self.timeout = timeout
|
|
72
|
+
self.auto_refresh = auto_refresh
|
|
73
|
+
self.retry_on_401 = retry_on_401
|
|
74
|
+
|
|
75
|
+
# Build SSL context: kwargs > provider config > global config
|
|
76
|
+
if ssl_verify is None and ssl_ca_bundle is None and hasattr(provider, "config"):
|
|
77
|
+
# Use provider's SSL settings if available
|
|
78
|
+
# Check for actual bool/str values (not MagicMock from tests)
|
|
79
|
+
provider_config = getattr(provider, "config", None)
|
|
80
|
+
provider_ssl_verify: bool | None = None
|
|
81
|
+
provider_ca_bundle: str | None = None
|
|
82
|
+
|
|
83
|
+
if provider_config is not None:
|
|
84
|
+
ssl_verify_attr = getattr(provider_config, "ssl_verify", None)
|
|
85
|
+
if isinstance(ssl_verify_attr, bool):
|
|
86
|
+
provider_ssl_verify = ssl_verify_attr
|
|
87
|
+
|
|
88
|
+
ca_bundle_attr = getattr(provider_config, "ssl_ca_bundle", None)
|
|
89
|
+
if isinstance(ca_bundle_attr, str):
|
|
90
|
+
provider_ca_bundle = ca_bundle_attr
|
|
91
|
+
|
|
92
|
+
self._ssl_context = build_ssl_context(
|
|
93
|
+
ssl_verify=provider_ssl_verify,
|
|
94
|
+
ssl_ca_bundle=provider_ca_bundle,
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
# Use explicit kwargs or fall back to global config
|
|
98
|
+
self._ssl_context = build_ssl_context(
|
|
99
|
+
ssl_verify=ssl_verify,
|
|
100
|
+
ssl_ca_bundle=ssl_ca_bundle,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self._sync_client: httpx.Client | None = None
|
|
104
|
+
self._async_client: httpx.AsyncClient | None = None
|
|
105
|
+
|
|
106
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
107
|
+
# Context managers
|
|
108
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def __enter__(self) -> Self:
|
|
111
|
+
"""Enter sync context manager."""
|
|
112
|
+
self._sync_client = httpx.Client(timeout=self.timeout, verify=self._ssl_context)
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def __exit__(
|
|
116
|
+
self,
|
|
117
|
+
exc_type: type[BaseException] | None,
|
|
118
|
+
exc_val: BaseException | None,
|
|
119
|
+
exc_tb: types.TracebackType | None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Exit sync context manager."""
|
|
122
|
+
if self._sync_client:
|
|
123
|
+
self._sync_client.close()
|
|
124
|
+
self._sync_client = None
|
|
125
|
+
|
|
126
|
+
async def __aenter__(self) -> Self:
|
|
127
|
+
"""Enter async context manager."""
|
|
128
|
+
self._async_client = httpx.AsyncClient(timeout=self.timeout, verify=self._ssl_context)
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
async def __aexit__(
|
|
132
|
+
self,
|
|
133
|
+
exc_type: type[BaseException] | None,
|
|
134
|
+
exc_val: BaseException | None,
|
|
135
|
+
exc_tb: types.TracebackType | None,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Exit async context manager."""
|
|
138
|
+
if self._async_client:
|
|
139
|
+
await self._async_client.aclose()
|
|
140
|
+
self._async_client = None
|
|
141
|
+
|
|
142
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
143
|
+
# Token handling
|
|
144
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
def _get_auth_header(self) -> dict[str, str]:
|
|
147
|
+
"""Get Authorization header with current token.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dict with Authorization header.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
TokenExpiredError: If no valid token is available.
|
|
154
|
+
"""
|
|
155
|
+
token = self.provider.get_token(auto_refresh=self.auto_refresh)
|
|
156
|
+
|
|
157
|
+
if token is None:
|
|
158
|
+
raise TokenExpiredError("No token available - authentication required")
|
|
159
|
+
|
|
160
|
+
if token.is_expired and not token.is_refreshable:
|
|
161
|
+
raise TokenExpiredError("Token expired and cannot be refreshed")
|
|
162
|
+
|
|
163
|
+
# Extract string value from TokenType enum or use as-is if already a string
|
|
164
|
+
token_type = token.token_type.value if isinstance(token.token_type, Enum) else token.token_type
|
|
165
|
+
|
|
166
|
+
return {"Authorization": f"{token_type} {token.access_token}"}
|
|
167
|
+
|
|
168
|
+
def _should_retry(self, response: httpx.Response, retried: bool) -> bool:
|
|
169
|
+
"""Check if request should be retried after 401."""
|
|
170
|
+
return (
|
|
171
|
+
self.retry_on_401
|
|
172
|
+
and not retried
|
|
173
|
+
and response.status_code == HTTPStatus.UNAUTHORIZED
|
|
174
|
+
and self.provider.get_token(auto_refresh=False) is not None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
178
|
+
# Sync HTTP methods
|
|
179
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
def _request(
|
|
182
|
+
self,
|
|
183
|
+
method: str,
|
|
184
|
+
url: str,
|
|
185
|
+
*,
|
|
186
|
+
_retried: bool = False,
|
|
187
|
+
**kwargs: Any,
|
|
188
|
+
) -> httpx.Response:
|
|
189
|
+
"""Make an authenticated HTTP request (sync).
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
method: HTTP method.
|
|
193
|
+
url: Request URL.
|
|
194
|
+
_retried: Internal flag to prevent infinite retry.
|
|
195
|
+
**kwargs: Additional arguments for httpx.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
HTTP response.
|
|
199
|
+
"""
|
|
200
|
+
if self._sync_client is None:
|
|
201
|
+
msg = "Session not initialized - use 'with AuthSession(...) as session:'"
|
|
202
|
+
raise AuthError(msg)
|
|
203
|
+
|
|
204
|
+
# Merge auth header with any existing headers
|
|
205
|
+
headers = kwargs.pop("headers", {})
|
|
206
|
+
headers.update(self._get_auth_header())
|
|
207
|
+
kwargs["headers"] = headers
|
|
208
|
+
|
|
209
|
+
if logger.isEnabledFor(TRACE_LEVEL):
|
|
210
|
+
logger.log(TRACE_LEVEL, "[SESSION] %s %s", method, url)
|
|
211
|
+
|
|
212
|
+
response = self._sync_client.request(method, url, **kwargs)
|
|
213
|
+
|
|
214
|
+
if logger.isEnabledFor(TRACE_LEVEL):
|
|
215
|
+
logger.log(TRACE_LEVEL, "[SESSION] Response: %d %s", response.status_code, response.reason_phrase)
|
|
216
|
+
|
|
217
|
+
# Retry on 401 if configured
|
|
218
|
+
if self._should_retry(response, _retried):
|
|
219
|
+
logger.debug("Got 401, attempting token refresh and retry")
|
|
220
|
+
try:
|
|
221
|
+
self.provider.refresh()
|
|
222
|
+
return self._request(method, url, _retried=True, **kwargs)
|
|
223
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
224
|
+
# Intentional catch-all for best-effort refresh
|
|
225
|
+
logger.warning("Token refresh failed, returning original 401 response")
|
|
226
|
+
|
|
227
|
+
return response
|
|
228
|
+
|
|
229
|
+
def get(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
230
|
+
"""Make authenticated GET request."""
|
|
231
|
+
return self._request("GET", url, **kwargs)
|
|
232
|
+
|
|
233
|
+
def post(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
234
|
+
"""Make authenticated POST request."""
|
|
235
|
+
return self._request("POST", url, **kwargs)
|
|
236
|
+
|
|
237
|
+
def put(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
238
|
+
"""Make authenticated PUT request."""
|
|
239
|
+
return self._request("PUT", url, **kwargs)
|
|
240
|
+
|
|
241
|
+
def patch(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
242
|
+
"""Make authenticated PATCH request."""
|
|
243
|
+
return self._request("PATCH", url, **kwargs)
|
|
244
|
+
|
|
245
|
+
def delete(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
246
|
+
"""Make authenticated DELETE request."""
|
|
247
|
+
return self._request("DELETE", url, **kwargs)
|
|
248
|
+
|
|
249
|
+
def head(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
250
|
+
"""Make authenticated HEAD request."""
|
|
251
|
+
return self._request("HEAD", url, **kwargs)
|
|
252
|
+
|
|
253
|
+
def options(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
254
|
+
"""Make authenticated OPTIONS request."""
|
|
255
|
+
return self._request("OPTIONS", url, **kwargs)
|
|
256
|
+
|
|
257
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
258
|
+
# Async HTTP methods
|
|
259
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
async def _arequest(
|
|
262
|
+
self,
|
|
263
|
+
method: str,
|
|
264
|
+
url: str,
|
|
265
|
+
*,
|
|
266
|
+
_retried: bool = False,
|
|
267
|
+
**kwargs: Any,
|
|
268
|
+
) -> httpx.Response:
|
|
269
|
+
"""Make an authenticated HTTP request (async).
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
method: HTTP method.
|
|
273
|
+
url: Request URL.
|
|
274
|
+
_retried: Internal flag to prevent infinite retry.
|
|
275
|
+
**kwargs: Additional arguments for httpx.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
HTTP response.
|
|
279
|
+
"""
|
|
280
|
+
if self._async_client is None:
|
|
281
|
+
msg = "Session not initialized - use 'async with AuthSession(...) as session:'"
|
|
282
|
+
raise AuthError(msg)
|
|
283
|
+
|
|
284
|
+
# Merge auth header with any existing headers
|
|
285
|
+
headers = kwargs.pop("headers", {})
|
|
286
|
+
headers.update(self._get_auth_header())
|
|
287
|
+
kwargs["headers"] = headers
|
|
288
|
+
|
|
289
|
+
if logger.isEnabledFor(TRACE_LEVEL):
|
|
290
|
+
logger.log(TRACE_LEVEL, "[SESSION] %s %s (async)", method, url)
|
|
291
|
+
|
|
292
|
+
response = await self._async_client.request(method, url, **kwargs)
|
|
293
|
+
|
|
294
|
+
if logger.isEnabledFor(TRACE_LEVEL):
|
|
295
|
+
logger.log(TRACE_LEVEL, "[SESSION] Response: %d %s", response.status_code, response.reason_phrase)
|
|
296
|
+
|
|
297
|
+
# Retry on 401 if configured
|
|
298
|
+
if self._should_retry(response, _retried):
|
|
299
|
+
logger.debug("Got 401, attempting token refresh and retry")
|
|
300
|
+
try:
|
|
301
|
+
self.provider.refresh()
|
|
302
|
+
return await self._arequest(method, url, _retried=True, **kwargs)
|
|
303
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
304
|
+
# Intentional catch-all for best-effort refresh
|
|
305
|
+
logger.warning("Token refresh failed, returning original 401 response")
|
|
306
|
+
|
|
307
|
+
return response
|
|
308
|
+
|
|
309
|
+
async def aget(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
310
|
+
"""Make authenticated async GET request."""
|
|
311
|
+
return await self._arequest("GET", url, **kwargs)
|
|
312
|
+
|
|
313
|
+
async def apost(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
314
|
+
"""Make authenticated async POST request."""
|
|
315
|
+
return await self._arequest("POST", url, **kwargs)
|
|
316
|
+
|
|
317
|
+
async def aput(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
318
|
+
"""Make authenticated async PUT request."""
|
|
319
|
+
return await self._arequest("PUT", url, **kwargs)
|
|
320
|
+
|
|
321
|
+
async def apatch(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
322
|
+
"""Make authenticated async PATCH request."""
|
|
323
|
+
return await self._arequest("PATCH", url, **kwargs)
|
|
324
|
+
|
|
325
|
+
async def adelete(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
326
|
+
"""Make authenticated async DELETE request."""
|
|
327
|
+
return await self._arequest("DELETE", url, **kwargs)
|
|
328
|
+
|
|
329
|
+
async def ahead(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
330
|
+
"""Make authenticated async HEAD request."""
|
|
331
|
+
return await self._arequest("HEAD", url, **kwargs)
|
|
332
|
+
|
|
333
|
+
async def aoptions(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
334
|
+
"""Make authenticated async OPTIONS request."""
|
|
335
|
+
return await self._arequest("OPTIONS", url, **kwargs)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
__all__ = ["AuthSession"]
|