@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.
Files changed (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. 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
+ ]