@lovrozagar/honey 0.1.0
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.
- package/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, AsyncIterator
|
|
6
|
+
|
|
7
|
+
from ._transport import (
|
|
8
|
+
TransportAdapter,
|
|
9
|
+
TransportConn,
|
|
10
|
+
TransportKind,
|
|
11
|
+
TransportOpts,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConnectionState(str, Enum):
|
|
16
|
+
CONNECTING = "connecting"
|
|
17
|
+
CONNECTED = "connected"
|
|
18
|
+
RECONNECTING = "reconnecting"
|
|
19
|
+
DISCONNECTED = "disconnected"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResumableConnection:
|
|
23
|
+
"""Orchestrates a pluggable TransportAdapter chain.
|
|
24
|
+
|
|
25
|
+
Mirrors TS `createResumableConnection` + Rust `ResumableConnection::connect`:
|
|
26
|
+
- Ordered `transports` chain tried in order on first connect.
|
|
27
|
+
- First successful adapter index memoized in `_proven_index`.
|
|
28
|
+
- On reconnect, proven adapter tried first; full chain on failure.
|
|
29
|
+
- State transitions drive `.state` property.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
base_url: str,
|
|
35
|
+
transports: list[TransportAdapter],
|
|
36
|
+
opts: TransportOpts | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
if not transports:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"ResumableConnection requires at least one transport adapter",
|
|
41
|
+
)
|
|
42
|
+
self._url = base_url
|
|
43
|
+
self._transports = transports
|
|
44
|
+
self._opts = opts or TransportOpts()
|
|
45
|
+
self._state: ConnectionState = ConnectionState.DISCONNECTED
|
|
46
|
+
self._conn: TransportConn | None = None
|
|
47
|
+
self._proven_index: int | None = None
|
|
48
|
+
self._reconnect_attempts: int = 0
|
|
49
|
+
self._closed: bool = False
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def state(self) -> ConnectionState:
|
|
53
|
+
return self._state
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def proven_transport(self) -> TransportKind | None:
|
|
57
|
+
if self._proven_index is None:
|
|
58
|
+
return None
|
|
59
|
+
if self._proven_index >= len(self._transports):
|
|
60
|
+
return None
|
|
61
|
+
return self._transports[self._proven_index].kind()
|
|
62
|
+
|
|
63
|
+
async def connect(self) -> None:
|
|
64
|
+
if self._closed:
|
|
65
|
+
raise RuntimeError("ResumableConnection is closed")
|
|
66
|
+
if self._state == ConnectionState.CONNECTED:
|
|
67
|
+
return
|
|
68
|
+
has_prior = self._conn is not None or self._proven_index is not None
|
|
69
|
+
self._state = (
|
|
70
|
+
ConnectionState.RECONNECTING if has_prior else ConnectionState.CONNECTING
|
|
71
|
+
)
|
|
72
|
+
await self._open_chain()
|
|
73
|
+
|
|
74
|
+
async def _open_chain(self) -> None:
|
|
75
|
+
last_err: BaseException | None = None
|
|
76
|
+
if (
|
|
77
|
+
self._proven_index is not None
|
|
78
|
+
and self._proven_index < len(self._transports)
|
|
79
|
+
):
|
|
80
|
+
adapter = self._transports[self._proven_index]
|
|
81
|
+
try:
|
|
82
|
+
self._conn = await adapter.connect(self._url, self._opts)
|
|
83
|
+
self._state = ConnectionState.CONNECTED
|
|
84
|
+
return
|
|
85
|
+
except BaseException as exc:
|
|
86
|
+
last_err = exc
|
|
87
|
+
for idx, adapter in enumerate(self._transports):
|
|
88
|
+
if idx == self._proven_index:
|
|
89
|
+
continue
|
|
90
|
+
try:
|
|
91
|
+
self._conn = await adapter.connect(self._url, self._opts)
|
|
92
|
+
self._proven_index = idx
|
|
93
|
+
self._state = ConnectionState.CONNECTED
|
|
94
|
+
return
|
|
95
|
+
except BaseException as exc:
|
|
96
|
+
last_err = exc
|
|
97
|
+
self._state = ConnectionState.DISCONNECTED
|
|
98
|
+
if last_err is not None:
|
|
99
|
+
raise last_err
|
|
100
|
+
raise RuntimeError("no transports configured")
|
|
101
|
+
|
|
102
|
+
async def send(self, data: Any) -> None:
|
|
103
|
+
if self._conn is None:
|
|
104
|
+
raise RuntimeError("not connected")
|
|
105
|
+
await self._conn.send(data)
|
|
106
|
+
|
|
107
|
+
async def close(self) -> None:
|
|
108
|
+
self._closed = True
|
|
109
|
+
self._state = ConnectionState.DISCONNECTED
|
|
110
|
+
if self._conn is not None:
|
|
111
|
+
conn = self._conn
|
|
112
|
+
self._conn = None
|
|
113
|
+
try:
|
|
114
|
+
await conn.close()
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def __aiter__(self) -> AsyncIterator[Any]:
|
|
119
|
+
return self._iter()
|
|
120
|
+
|
|
121
|
+
async def _iter(self) -> AsyncIterator[Any]:
|
|
122
|
+
if self._conn is None and not self._closed:
|
|
123
|
+
await self.connect()
|
|
124
|
+
while not self._closed:
|
|
125
|
+
conn = self._conn
|
|
126
|
+
if conn is None:
|
|
127
|
+
return
|
|
128
|
+
try:
|
|
129
|
+
value = await conn.recv()
|
|
130
|
+
except StopAsyncIteration:
|
|
131
|
+
return
|
|
132
|
+
except asyncio.CancelledError:
|
|
133
|
+
raise
|
|
134
|
+
except BaseException:
|
|
135
|
+
""" transport died — invalidate proven, retry full chain after delay """
|
|
136
|
+
self._reconnect_attempts += 1
|
|
137
|
+
max_attempts = self._opts.max_reconnect_attempts
|
|
138
|
+
if max_attempts > 0 and self._reconnect_attempts > max_attempts:
|
|
139
|
+
self._state = ConnectionState.DISCONNECTED
|
|
140
|
+
return
|
|
141
|
+
delay = self._opts.reconnect_delay_ms / 1000.0
|
|
142
|
+
if delay > 0:
|
|
143
|
+
await asyncio.sleep(delay)
|
|
144
|
+
self._state = ConnectionState.RECONNECTING
|
|
145
|
+
self._conn = None
|
|
146
|
+
self._proven_index = None
|
|
147
|
+
try:
|
|
148
|
+
await self._open_chain()
|
|
149
|
+
except BaseException:
|
|
150
|
+
self._state = ConnectionState.DISCONNECTED
|
|
151
|
+
return
|
|
152
|
+
continue
|
|
153
|
+
yield value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = ["ConnectionState", "ResumableConnection"]
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Awaitable, Callable, Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._errors import APIStatusError, _STATUS_ERROR_MAP
|
|
12
|
+
from ._invalidation import _RequestMeta
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
DEFAULT_TIMEOUT = 30.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RequestContext:
|
|
21
|
+
method: str
|
|
22
|
+
url: str
|
|
23
|
+
headers: dict[str, str]
|
|
24
|
+
body: Any
|
|
25
|
+
is_retry: bool = False
|
|
26
|
+
selector: str | None = None
|
|
27
|
+
is_stale: bool | None = None
|
|
28
|
+
invalidated_by: list[str] | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ResponseContext:
|
|
33
|
+
response: httpx.Response
|
|
34
|
+
body: bytes
|
|
35
|
+
parsed: Any
|
|
36
|
+
is_retry: bool = False
|
|
37
|
+
selector: str | None = None
|
|
38
|
+
is_stale: bool | None = None
|
|
39
|
+
invalidated_by: list[str] | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class LogEntry:
|
|
44
|
+
level: str
|
|
45
|
+
event: str
|
|
46
|
+
operation: str
|
|
47
|
+
duration_ms: int
|
|
48
|
+
status: int | None = None
|
|
49
|
+
error: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class InvalidationConfig:
|
|
54
|
+
stale_time: float = 0.0
|
|
55
|
+
stale_max_entries: int = 1000
|
|
56
|
+
max_sources_per_target: int = 16
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ClientConfig:
|
|
61
|
+
base_url: str
|
|
62
|
+
bearer_token: str | None = None
|
|
63
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
64
|
+
timeout: float | None = DEFAULT_TIMEOUT
|
|
65
|
+
throw_on_error: bool = True
|
|
66
|
+
on_auth_expired: Callable[[], Awaitable[str | None]] | None = None
|
|
67
|
+
auth_header_name: str | None = None
|
|
68
|
+
auth_header_prefix: str | None = None
|
|
69
|
+
transport: httpx.AsyncBaseTransport | None = None
|
|
70
|
+
sync_transport: httpx.BaseTransport | None = None
|
|
71
|
+
on_request: list[Callable[[RequestContext], Awaitable[None]]] | None = None
|
|
72
|
+
on_response: list[Callable[[ResponseContext], Awaitable[None]]] | None = None
|
|
73
|
+
on_request_sync: list[Callable[[RequestContext], None]] | None = None
|
|
74
|
+
on_response_sync: list[Callable[[ResponseContext], None]] | None = None
|
|
75
|
+
on_log: Callable[[LogEntry], None] | None = None
|
|
76
|
+
invalidation: InvalidationConfig | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _emit_log(config: ClientConfig, entry: LogEntry) -> None:
|
|
80
|
+
if config.on_log is None:
|
|
81
|
+
return
|
|
82
|
+
try:
|
|
83
|
+
config.on_log(entry)
|
|
84
|
+
except Exception:
|
|
85
|
+
""" logger must never break request path """
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _elapsed_ms(start: float) -> int:
|
|
90
|
+
return round((time.monotonic() - start) * 1000)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _err_status(exc: BaseException) -> int | None:
|
|
94
|
+
status = getattr(exc, "status", None)
|
|
95
|
+
return status if isinstance(status, int) else None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class SDKResult(Generic[T]):
|
|
100
|
+
data: T | None
|
|
101
|
+
error: Any
|
|
102
|
+
status: int
|
|
103
|
+
response: httpx.Response
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _auth_header_name(config: ClientConfig) -> str:
|
|
107
|
+
return config.auth_header_name or "Authorization"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _build_headers(
|
|
111
|
+
config: ClientConfig,
|
|
112
|
+
extra: dict[str, str] | None = None,
|
|
113
|
+
bearer_token: str | None = None,
|
|
114
|
+
) -> dict[str, str]:
|
|
115
|
+
merged: dict[str, str] = {**config.headers}
|
|
116
|
+
token = bearer_token if bearer_token is not None else config.bearer_token
|
|
117
|
+
if token:
|
|
118
|
+
prefix = (
|
|
119
|
+
config.auth_header_prefix
|
|
120
|
+
if config.auth_header_prefix is not None
|
|
121
|
+
else "Bearer "
|
|
122
|
+
)
|
|
123
|
+
merged[_auth_header_name(config)] = f"{prefix}{token}"
|
|
124
|
+
if extra:
|
|
125
|
+
merged = {**merged, **extra}
|
|
126
|
+
return merged
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _build_url(base_url: str, path: str, params: dict[str, Any] | None = None) -> str:
|
|
130
|
+
url = base_url.rstrip("/") + path
|
|
131
|
+
if params:
|
|
132
|
+
query = httpx.QueryParams(
|
|
133
|
+
{k: v for k, v in params.items() if v is not None}
|
|
134
|
+
)
|
|
135
|
+
if query:
|
|
136
|
+
url = f"{url}?{query}"
|
|
137
|
+
return url
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_body(response: httpx.Response) -> Any:
|
|
141
|
+
content_type = response.headers.get("content-type", "")
|
|
142
|
+
if "application/json" in content_type:
|
|
143
|
+
try:
|
|
144
|
+
return response.json()
|
|
145
|
+
except Exception:
|
|
146
|
+
return None
|
|
147
|
+
return response.text
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _raise_for_status(response: httpx.Response, body: Any = None) -> None:
|
|
151
|
+
if response.status_code < 400:
|
|
152
|
+
return
|
|
153
|
+
parsed = body if body is not None else _parse_body(response)
|
|
154
|
+
raw = response.content
|
|
155
|
+
status = response.status_code
|
|
156
|
+
msg_val = parsed.get("message") if isinstance(parsed, dict) else None
|
|
157
|
+
if isinstance(msg_val, str):
|
|
158
|
+
message = msg_val
|
|
159
|
+
elif isinstance(parsed, str):
|
|
160
|
+
message = parsed
|
|
161
|
+
elif parsed is not None:
|
|
162
|
+
message = str(parsed)
|
|
163
|
+
else:
|
|
164
|
+
message = f"HTTP {status}"
|
|
165
|
+
error_cls = _STATUS_ERROR_MAP.get(status, APIStatusError)
|
|
166
|
+
raise error_cls(
|
|
167
|
+
status=status,
|
|
168
|
+
body=raw,
|
|
169
|
+
data=parsed,
|
|
170
|
+
response=response,
|
|
171
|
+
message=message[:512] if message else f"HTTP {status}",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _apply_meta_to_request_ctx(ctx: RequestContext, meta: _RequestMeta | None) -> None:
|
|
176
|
+
if meta is None:
|
|
177
|
+
return
|
|
178
|
+
ctx.selector = meta.selector
|
|
179
|
+
ctx.is_stale = meta.is_stale
|
|
180
|
+
ctx.invalidated_by = meta.invalidated_by
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _apply_meta_to_response_ctx(
|
|
184
|
+
ctx: ResponseContext, meta: _RequestMeta | None,
|
|
185
|
+
) -> None:
|
|
186
|
+
if meta is None:
|
|
187
|
+
return
|
|
188
|
+
ctx.selector = meta.selector
|
|
189
|
+
ctx.is_stale = meta.is_stale
|
|
190
|
+
ctx.invalidated_by = meta.invalidated_by
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def _do_request_async(
|
|
194
|
+
client: httpx.AsyncClient,
|
|
195
|
+
config: ClientConfig,
|
|
196
|
+
method: str,
|
|
197
|
+
url: str,
|
|
198
|
+
headers: dict[str, str],
|
|
199
|
+
json: Any = None,
|
|
200
|
+
content: Any = None,
|
|
201
|
+
params: dict[str, Any] | None = None,
|
|
202
|
+
timeout: float | None = None,
|
|
203
|
+
cancel_token: threading.Event | None = None,
|
|
204
|
+
operation: str = "",
|
|
205
|
+
request_meta: _RequestMeta | None = None,
|
|
206
|
+
) -> httpx.Response:
|
|
207
|
+
if cancel_token is not None and cancel_token.is_set():
|
|
208
|
+
raise asyncio.CancelledError()
|
|
209
|
+
timeout_val = timeout if timeout is not None else config.timeout
|
|
210
|
+
start = time.monotonic()
|
|
211
|
+
|
|
212
|
+
req_ctx = RequestContext(method=method, url=url, headers=dict(headers), body=json)
|
|
213
|
+
_apply_meta_to_request_ctx(req_ctx, request_meta)
|
|
214
|
+
if config.on_request:
|
|
215
|
+
for hook in config.on_request:
|
|
216
|
+
await hook(req_ctx)
|
|
217
|
+
|
|
218
|
+
_emit_log(
|
|
219
|
+
config,
|
|
220
|
+
LogEntry(
|
|
221
|
+
level="debug",
|
|
222
|
+
event="request_start",
|
|
223
|
+
operation=operation,
|
|
224
|
+
duration_ms=0,
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
response = await client.request(
|
|
230
|
+
req_ctx.method,
|
|
231
|
+
req_ctx.url,
|
|
232
|
+
headers=req_ctx.headers,
|
|
233
|
+
json=req_ctx.body if content is None else None,
|
|
234
|
+
content=content,
|
|
235
|
+
params={k: v for k, v in (params or {}).items() if v is not None} or None,
|
|
236
|
+
timeout=httpx.Timeout(timeout_val) if timeout_val is not None else None,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if config.on_response:
|
|
240
|
+
raw = response.content
|
|
241
|
+
parsed = _parse_body(response)
|
|
242
|
+
resp_ctx = ResponseContext(response=response, body=raw, parsed=parsed)
|
|
243
|
+
_apply_meta_to_response_ctx(resp_ctx, request_meta)
|
|
244
|
+
for hook in config.on_response:
|
|
245
|
+
await hook(resp_ctx)
|
|
246
|
+
|
|
247
|
+
if response.status_code == 401 and config.on_auth_expired is not None:
|
|
248
|
+
new_token = await config.on_auth_expired()
|
|
249
|
+
if new_token is not None:
|
|
250
|
+
current_auth_name = _auth_header_name(config).lower()
|
|
251
|
+
extra_no_auth = {
|
|
252
|
+
k: v for k, v in req_ctx.headers.items()
|
|
253
|
+
if k.lower() != current_auth_name
|
|
254
|
+
}
|
|
255
|
+
retry_headers = _build_headers(
|
|
256
|
+
config, extra=extra_no_auth or None, bearer_token=new_token,
|
|
257
|
+
)
|
|
258
|
+
retry_params = {
|
|
259
|
+
k: v for k, v in (params or {}).items() if v is not None
|
|
260
|
+
} or None
|
|
261
|
+
|
|
262
|
+
retry_ctx = RequestContext(
|
|
263
|
+
method=req_ctx.method,
|
|
264
|
+
url=req_ctx.url,
|
|
265
|
+
headers=retry_headers,
|
|
266
|
+
body=req_ctx.body,
|
|
267
|
+
is_retry=True,
|
|
268
|
+
)
|
|
269
|
+
_apply_meta_to_request_ctx(retry_ctx, request_meta)
|
|
270
|
+
if config.on_request:
|
|
271
|
+
for hook in config.on_request:
|
|
272
|
+
await hook(retry_ctx)
|
|
273
|
+
|
|
274
|
+
response = await client.request(
|
|
275
|
+
retry_ctx.method,
|
|
276
|
+
retry_ctx.url,
|
|
277
|
+
headers=retry_ctx.headers,
|
|
278
|
+
json=retry_ctx.body,
|
|
279
|
+
params=retry_params,
|
|
280
|
+
timeout=(
|
|
281
|
+
httpx.Timeout(timeout_val) if timeout_val is not None else None
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if config.on_response:
|
|
286
|
+
raw = response.content
|
|
287
|
+
parsed = _parse_body(response)
|
|
288
|
+
resp_ctx = ResponseContext(
|
|
289
|
+
response=response, body=raw, parsed=parsed, is_retry=True,
|
|
290
|
+
)
|
|
291
|
+
_apply_meta_to_response_ctx(resp_ctx, request_meta)
|
|
292
|
+
for hook in config.on_response:
|
|
293
|
+
await hook(resp_ctx)
|
|
294
|
+
|
|
295
|
+
_emit_log(
|
|
296
|
+
config,
|
|
297
|
+
LogEntry(
|
|
298
|
+
level="warn" if response.status_code >= 400 else "info",
|
|
299
|
+
event="response_received",
|
|
300
|
+
operation=operation,
|
|
301
|
+
duration_ms=_elapsed_ms(start),
|
|
302
|
+
status=response.status_code,
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
return response
|
|
306
|
+
except BaseException as exc:
|
|
307
|
+
_emit_log(
|
|
308
|
+
config,
|
|
309
|
+
LogEntry(
|
|
310
|
+
level="error",
|
|
311
|
+
event="error",
|
|
312
|
+
operation=operation,
|
|
313
|
+
duration_ms=_elapsed_ms(start),
|
|
314
|
+
status=_err_status(exc),
|
|
315
|
+
error=repr(exc),
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
raise
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _do_request_sync(
|
|
322
|
+
client: httpx.Client,
|
|
323
|
+
config: ClientConfig,
|
|
324
|
+
method: str,
|
|
325
|
+
url: str,
|
|
326
|
+
headers: dict[str, str],
|
|
327
|
+
json: Any = None,
|
|
328
|
+
content: Any = None,
|
|
329
|
+
params: dict[str, Any] | None = None,
|
|
330
|
+
timeout: float | None = None,
|
|
331
|
+
cancel_token: threading.Event | None = None,
|
|
332
|
+
operation: str = "",
|
|
333
|
+
request_meta: _RequestMeta | None = None,
|
|
334
|
+
) -> httpx.Response:
|
|
335
|
+
if cancel_token is not None and cancel_token.is_set():
|
|
336
|
+
raise asyncio.CancelledError()
|
|
337
|
+
timeout_val = timeout if timeout is not None else config.timeout
|
|
338
|
+
start = time.monotonic()
|
|
339
|
+
|
|
340
|
+
req_ctx = RequestContext(method=method, url=url, headers=dict(headers), body=json)
|
|
341
|
+
_apply_meta_to_request_ctx(req_ctx, request_meta)
|
|
342
|
+
if config.on_request_sync:
|
|
343
|
+
for hook in config.on_request_sync:
|
|
344
|
+
hook(req_ctx)
|
|
345
|
+
|
|
346
|
+
_emit_log(
|
|
347
|
+
config,
|
|
348
|
+
LogEntry(
|
|
349
|
+
level="debug",
|
|
350
|
+
event="request_start",
|
|
351
|
+
operation=operation,
|
|
352
|
+
duration_ms=0,
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
response = client.request(
|
|
358
|
+
req_ctx.method,
|
|
359
|
+
req_ctx.url,
|
|
360
|
+
headers=req_ctx.headers,
|
|
361
|
+
json=req_ctx.body if content is None else None,
|
|
362
|
+
content=content,
|
|
363
|
+
params={k: v for k, v in (params or {}).items() if v is not None} or None,
|
|
364
|
+
timeout=httpx.Timeout(timeout_val) if timeout_val is not None else None,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if config.on_response_sync:
|
|
368
|
+
raw = response.content
|
|
369
|
+
parsed = _parse_body(response)
|
|
370
|
+
resp_ctx = ResponseContext(response=response, body=raw, parsed=parsed)
|
|
371
|
+
_apply_meta_to_response_ctx(resp_ctx, request_meta)
|
|
372
|
+
for hook in config.on_response_sync:
|
|
373
|
+
hook(resp_ctx)
|
|
374
|
+
|
|
375
|
+
if response.status_code == 401 and config.on_auth_expired is not None:
|
|
376
|
+
try:
|
|
377
|
+
_loop = asyncio.new_event_loop()
|
|
378
|
+
try:
|
|
379
|
+
new_token = _loop.run_until_complete(config.on_auth_expired())
|
|
380
|
+
finally:
|
|
381
|
+
_loop.close()
|
|
382
|
+
except RuntimeError:
|
|
383
|
+
new_token = None
|
|
384
|
+
if new_token is not None:
|
|
385
|
+
current_auth_name = _auth_header_name(config).lower()
|
|
386
|
+
extra_no_auth = {
|
|
387
|
+
k: v for k, v in req_ctx.headers.items()
|
|
388
|
+
if k.lower() != current_auth_name
|
|
389
|
+
}
|
|
390
|
+
retry_headers = _build_headers(
|
|
391
|
+
config, extra=extra_no_auth or None, bearer_token=new_token,
|
|
392
|
+
)
|
|
393
|
+
retry_params = {
|
|
394
|
+
k: v for k, v in (params or {}).items() if v is not None
|
|
395
|
+
} or None
|
|
396
|
+
|
|
397
|
+
retry_ctx = RequestContext(
|
|
398
|
+
method=req_ctx.method,
|
|
399
|
+
url=req_ctx.url,
|
|
400
|
+
headers=retry_headers,
|
|
401
|
+
body=req_ctx.body,
|
|
402
|
+
is_retry=True,
|
|
403
|
+
)
|
|
404
|
+
_apply_meta_to_request_ctx(retry_ctx, request_meta)
|
|
405
|
+
if config.on_request_sync:
|
|
406
|
+
for hook in config.on_request_sync:
|
|
407
|
+
hook(retry_ctx)
|
|
408
|
+
|
|
409
|
+
response = client.request(
|
|
410
|
+
retry_ctx.method,
|
|
411
|
+
retry_ctx.url,
|
|
412
|
+
headers=retry_ctx.headers,
|
|
413
|
+
json=retry_ctx.body,
|
|
414
|
+
params=retry_params,
|
|
415
|
+
timeout=(
|
|
416
|
+
httpx.Timeout(timeout_val) if timeout_val is not None else None
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if config.on_response_sync:
|
|
421
|
+
raw = response.content
|
|
422
|
+
parsed = _parse_body(response)
|
|
423
|
+
resp_ctx = ResponseContext(
|
|
424
|
+
response=response, body=raw, parsed=parsed, is_retry=True,
|
|
425
|
+
)
|
|
426
|
+
_apply_meta_to_response_ctx(resp_ctx, request_meta)
|
|
427
|
+
for hook in config.on_response_sync:
|
|
428
|
+
hook(resp_ctx)
|
|
429
|
+
|
|
430
|
+
_emit_log(
|
|
431
|
+
config,
|
|
432
|
+
LogEntry(
|
|
433
|
+
level="warn" if response.status_code >= 400 else "info",
|
|
434
|
+
event="response_received",
|
|
435
|
+
operation=operation,
|
|
436
|
+
duration_ms=_elapsed_ms(start),
|
|
437
|
+
status=response.status_code,
|
|
438
|
+
),
|
|
439
|
+
)
|
|
440
|
+
return response
|
|
441
|
+
except BaseException as exc:
|
|
442
|
+
_emit_log(
|
|
443
|
+
config,
|
|
444
|
+
LogEntry(
|
|
445
|
+
level="error",
|
|
446
|
+
event="error",
|
|
447
|
+
operation=operation,
|
|
448
|
+
duration_ms=_elapsed_ms(start),
|
|
449
|
+
status=_err_status(exc),
|
|
450
|
+
error=repr(exc),
|
|
451
|
+
),
|
|
452
|
+
)
|
|
453
|
+
raise
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
__all__ = [
|
|
457
|
+
"ClientConfig",
|
|
458
|
+
"InvalidationConfig",
|
|
459
|
+
"LogEntry",
|
|
460
|
+
"RequestContext",
|
|
461
|
+
"ResponseContext",
|
|
462
|
+
"SDKResult",
|
|
463
|
+
"_build_headers",
|
|
464
|
+
"_build_url",
|
|
465
|
+
"_do_request_async",
|
|
466
|
+
"_do_request_sync",
|
|
467
|
+
"_parse_body",
|
|
468
|
+
"_raise_for_status",
|
|
469
|
+
]
|