reflectapi-runtime 0.17.2a2__tar.gz → 0.17.2a3__tar.gz
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.
- reflectapi_runtime-0.17.2a3/.gitignore +1 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/PKG-INFO +1 -1
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/pyproject.toml +1 -1
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/__init__.py +6 -1
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/client.py +256 -125
- reflectapi_runtime-0.17.2a3/src/reflectapi_runtime/middleware.py +252 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/streaming.py +8 -5
- reflectapi_runtime-0.17.2a3/src/reflectapi_runtime/transport.py +65 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_batch.py +1 -1
- reflectapi_runtime-0.17.2a3/tests/test_client.py +909 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_edge_cases.py +23 -11
- reflectapi_runtime-0.17.2a3/tests/test_middleware.py +487 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_pydantic_serialization.py +30 -33
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_sse.py +2 -2
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_streaming.py +43 -0
- reflectapi_runtime-0.17.2a2/.gitignore +0 -5
- reflectapi_runtime-0.17.2a2/src/reflectapi_runtime/middleware.py +0 -258
- reflectapi_runtime-0.17.2a2/tests/test_client.py +0 -430
- reflectapi_runtime-0.17.2a2/tests/test_middleware.py +0 -704
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/README.md +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/auth.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/batch.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/exceptions.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/hypothesis_strategies.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/option.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/response.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/sse.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/testing.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/types.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/__init__.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_auth.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_auth_negative_cases.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_enhanced_features.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_exceptions.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_option.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_response.py +0 -0
- {reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/tests/test_testing.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uv.lock
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reflectapi-runtime
|
|
3
|
-
Version: 0.17.
|
|
3
|
+
Version: 0.17.2a3
|
|
4
4
|
Summary: Runtime library for ReflectAPI Python clients
|
|
5
5
|
Project-URL: Homepage, https://github.com/thepartly/reflectapi
|
|
6
6
|
Project-URL: Repository, https://github.com/thepartly/reflectapi
|
{reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/__init__.py
RENAMED
|
@@ -20,6 +20,7 @@ from .auth import (
|
|
|
20
20
|
)
|
|
21
21
|
from .batch import BatchClient
|
|
22
22
|
from .client import AsyncClientBase, ClientBase
|
|
23
|
+
from .transport import AsyncClient, Client, Request, Response
|
|
23
24
|
from .exceptions import (
|
|
24
25
|
ApiError,
|
|
25
26
|
ApplicationError,
|
|
@@ -55,7 +56,7 @@ from .testing import (
|
|
|
55
56
|
)
|
|
56
57
|
from .types import BatchResult, ReflectapiEmpty, ReflectapiInfallible
|
|
57
58
|
|
|
58
|
-
__version__ = "0.17.
|
|
59
|
+
__version__ = "0.17.2a3"
|
|
59
60
|
|
|
60
61
|
__all__ = [
|
|
61
62
|
# Authentication
|
|
@@ -78,12 +79,16 @@ __all__ = [
|
|
|
78
79
|
"ApplicationError",
|
|
79
80
|
"AsyncCassetteMiddleware",
|
|
80
81
|
"AsyncClientBase",
|
|
82
|
+
"AsyncClient",
|
|
81
83
|
"AsyncStreamingClient",
|
|
82
84
|
"BatchClient",
|
|
83
85
|
"BatchResult",
|
|
84
86
|
"CassetteClient",
|
|
85
87
|
"CassetteMiddleware",
|
|
88
|
+
"Client",
|
|
86
89
|
"ClientBase",
|
|
90
|
+
"Request",
|
|
91
|
+
"Response",
|
|
87
92
|
"HAS_HYPOTHESIS",
|
|
88
93
|
"AsyncMiddleware",
|
|
89
94
|
"MockClient",
|
{reflectapi_runtime-0.17.2a2 → reflectapi_runtime-0.17.2a3}/src/reflectapi_runtime/client.py
RENAMED
|
@@ -6,7 +6,7 @@ import datetime
|
|
|
6
6
|
import json
|
|
7
7
|
import time
|
|
8
8
|
from abc import ABC
|
|
9
|
-
from collections.abc import AsyncIterator, Iterator
|
|
9
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
|
10
10
|
from typing import Any, TypeVar, overload
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
@@ -24,6 +24,7 @@ from .middleware import (
|
|
|
24
24
|
from .option import serialize_option_dict
|
|
25
25
|
from .response import ApiResponse, TransportMetadata
|
|
26
26
|
from .sse import aparse_sse, parse_sse
|
|
27
|
+
from .transport import AsyncClient, Client, Request, Response
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
# Sentinel object to represent "no validation needed"
|
|
@@ -61,7 +62,7 @@ class ClientBase(ABC):
|
|
|
61
62
|
headers: dict[str, str] | None = None,
|
|
62
63
|
middleware: list[SyncMiddleware] | None = None,
|
|
63
64
|
auth: AuthHandler | httpx.Auth | None = None,
|
|
64
|
-
client: httpx.Client | None = None,
|
|
65
|
+
client: Client | httpx.Client | None = None,
|
|
65
66
|
) -> None:
|
|
66
67
|
self.base_url = base_url.rstrip("/")
|
|
67
68
|
self.middleware_chain = SyncMiddlewareChain(middleware or [])
|
|
@@ -165,7 +166,7 @@ class ClientBase(ABC):
|
|
|
165
166
|
@overload
|
|
166
167
|
def _make_request(
|
|
167
168
|
self,
|
|
168
|
-
|
|
169
|
+
|
|
169
170
|
path: str,
|
|
170
171
|
*,
|
|
171
172
|
params: dict[str, Any] | None = None,
|
|
@@ -178,7 +179,7 @@ class ClientBase(ABC):
|
|
|
178
179
|
@overload
|
|
179
180
|
def _make_request(
|
|
180
181
|
self,
|
|
181
|
-
|
|
182
|
+
|
|
182
183
|
path: str,
|
|
183
184
|
*,
|
|
184
185
|
params: dict[str, Any] | None = None,
|
|
@@ -191,7 +192,7 @@ class ClientBase(ABC):
|
|
|
191
192
|
@overload
|
|
192
193
|
def _make_request(
|
|
193
194
|
self,
|
|
194
|
-
|
|
195
|
+
|
|
195
196
|
path: str,
|
|
196
197
|
*,
|
|
197
198
|
params: dict[str, Any] | None = None,
|
|
@@ -204,7 +205,7 @@ class ClientBase(ABC):
|
|
|
204
205
|
@overload
|
|
205
206
|
def _make_request(
|
|
206
207
|
self,
|
|
207
|
-
|
|
208
|
+
|
|
208
209
|
path: str,
|
|
209
210
|
*,
|
|
210
211
|
params: dict[str, Any] | None = None,
|
|
@@ -216,7 +217,7 @@ class ClientBase(ABC):
|
|
|
216
217
|
@overload
|
|
217
218
|
def _make_request(
|
|
218
219
|
self,
|
|
219
|
-
|
|
220
|
+
|
|
220
221
|
path: str,
|
|
221
222
|
*,
|
|
222
223
|
params: dict[str, Any] | None = None,
|
|
@@ -229,7 +230,7 @@ class ClientBase(ABC):
|
|
|
229
230
|
@overload
|
|
230
231
|
def _make_request(
|
|
231
232
|
self,
|
|
232
|
-
|
|
233
|
+
|
|
233
234
|
path: str,
|
|
234
235
|
*,
|
|
235
236
|
params: dict[str, Any] | None = None,
|
|
@@ -241,7 +242,7 @@ class ClientBase(ABC):
|
|
|
241
242
|
|
|
242
243
|
def _validate_request_params(
|
|
243
244
|
self,
|
|
244
|
-
json_data:
|
|
245
|
+
json_data: Any | None,
|
|
245
246
|
json_model: BaseModel | None,
|
|
246
247
|
) -> None:
|
|
247
248
|
"""Validate request parameters for conflicts."""
|
|
@@ -321,58 +322,103 @@ class ClientBase(ABC):
|
|
|
321
322
|
|
|
322
323
|
return headers
|
|
323
324
|
|
|
324
|
-
def
|
|
325
|
+
def _build_client_request(
|
|
325
326
|
self,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
params: dict[str, Any] | None,
|
|
329
|
-
json_data: dict[str, Any] | None,
|
|
327
|
+
path: str,
|
|
328
|
+
json_data: Any | None,
|
|
330
329
|
json_model: BaseModel | None,
|
|
331
330
|
headers_model: BaseModel | None,
|
|
332
|
-
) ->
|
|
333
|
-
"""Build
|
|
331
|
+
) -> Request:
|
|
332
|
+
"""Build ReflectAPI transport request object."""
|
|
334
333
|
if json_model is not None:
|
|
335
|
-
# Serialize Pydantic model
|
|
336
334
|
content, base_headers = self._serialize_request_body(json_model)
|
|
337
335
|
headers = self._build_headers(base_headers, headers_model)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
url=url,
|
|
342
|
-
params=params,
|
|
343
|
-
content=content,
|
|
344
|
-
headers=headers,
|
|
345
|
-
)
|
|
346
|
-
else:
|
|
347
|
-
# Handle JSON data with Option types
|
|
348
|
-
if json_data is not None:
|
|
349
|
-
# Only serialize Option types for dictionaries (complex types)
|
|
350
|
-
# Primitive types (int, str, bool, etc.) should be passed directly
|
|
351
|
-
if isinstance(json_data, dict):
|
|
352
|
-
processed_json_data = serialize_option_dict(json_data)
|
|
353
|
-
else:
|
|
354
|
-
# Primitive types - pass through directly
|
|
355
|
-
processed_json_data = json_data
|
|
336
|
+
elif json_data is not None:
|
|
337
|
+
if isinstance(json_data, dict):
|
|
338
|
+
processed_json_data = serialize_option_dict(json_data)
|
|
356
339
|
else:
|
|
357
340
|
processed_json_data = json_data
|
|
358
|
-
|
|
359
|
-
|
|
341
|
+
content = json.dumps(
|
|
342
|
+
processed_json_data,
|
|
343
|
+
default=_json_serializer,
|
|
344
|
+
separators=(",", ":"),
|
|
345
|
+
).encode("utf-8")
|
|
346
|
+
headers = self._build_headers(
|
|
347
|
+
{"Content-Type": "application/json"}, headers_model
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
content = b""
|
|
360
351
|
headers = self._build_headers({}, headers_model)
|
|
361
352
|
|
|
362
|
-
|
|
363
|
-
method=method,
|
|
364
|
-
url=url,
|
|
365
|
-
params=params,
|
|
366
|
-
json=processed_json_data,
|
|
367
|
-
headers=headers if headers else None,
|
|
368
|
-
)
|
|
353
|
+
return Request(path=path, headers=headers, body=content)
|
|
369
354
|
|
|
370
|
-
def
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
355
|
+
def _build_httpx_request(
|
|
356
|
+
self,
|
|
357
|
+
request: Request,
|
|
358
|
+
url: str,
|
|
359
|
+
params: dict[str, Any] | None,
|
|
360
|
+
) -> httpx.Request:
|
|
361
|
+
"""Materialise an httpx.Request from a transport Request.
|
|
362
|
+
|
|
363
|
+
Method is always POST by design.
|
|
364
|
+
"""
|
|
365
|
+
request_kwargs: dict[str, Any] = {
|
|
366
|
+
"method": "POST",
|
|
367
|
+
"url": url,
|
|
368
|
+
"params": params,
|
|
369
|
+
"headers": request.headers if request.headers else None,
|
|
370
|
+
}
|
|
371
|
+
if request.body:
|
|
372
|
+
request_kwargs["content"] = request.body
|
|
373
|
+
return self._client.build_request(**request_kwargs)
|
|
374
|
+
|
|
375
|
+
def _make_terminal(
|
|
376
|
+
self,
|
|
377
|
+
params: dict[str, Any] | None,
|
|
378
|
+
) -> Callable[[Request], Response]:
|
|
379
|
+
"""Return the chain-terminal handler for the active transport.
|
|
380
|
+
|
|
381
|
+
For an ``httpx.Client``, the terminal lifts the Request into an
|
|
382
|
+
``httpx.Request``, sends it, and surfaces the real
|
|
383
|
+
``httpx.Response`` via ``Response.raw`` so callers can read its
|
|
384
|
+
``request`` / ``extensions`` / ``history`` from
|
|
385
|
+
:class:`TransportMetadata`.
|
|
386
|
+
|
|
387
|
+
For a custom :class:`Client`, the terminal accepts either a
|
|
388
|
+
:class:`Response` (the Protocol contract) or an ``httpx.Response``;
|
|
389
|
+
the latter is adapted at the boundary for transports that wrap
|
|
390
|
+
httpx directly. *Transitional:* the httpx-shaped path is a
|
|
391
|
+
backward-compat shim for adapters written against the previous
|
|
392
|
+
runtime; new code should return a :class:`Response`.
|
|
393
|
+
"""
|
|
394
|
+
if isinstance(self._client, httpx.Client):
|
|
395
|
+
base_url = self.base_url
|
|
396
|
+
|
|
397
|
+
def terminal(req: Request) -> Response:
|
|
398
|
+
url = f"{base_url}/{req.path.lstrip('/')}"
|
|
399
|
+
httpx_req = self._build_httpx_request(req, url, params)
|
|
400
|
+
httpx_resp = self._client.send(httpx_req)
|
|
401
|
+
return Response(
|
|
402
|
+
status=httpx_resp.status_code,
|
|
403
|
+
headers=httpx_resp.headers,
|
|
404
|
+
body=httpx_resp.content,
|
|
405
|
+
raw=httpx_resp,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return terminal
|
|
409
|
+
|
|
410
|
+
def terminal(req: Request) -> Response:
|
|
411
|
+
response = self._client.request(req)
|
|
412
|
+
if isinstance(response, httpx.Response):
|
|
413
|
+
return Response(
|
|
414
|
+
status=response.status_code,
|
|
415
|
+
headers=response.headers,
|
|
416
|
+
body=response.content,
|
|
417
|
+
raw=response,
|
|
418
|
+
)
|
|
419
|
+
return response
|
|
420
|
+
|
|
421
|
+
return terminal
|
|
376
422
|
|
|
377
423
|
def _handle_error_response(
|
|
378
424
|
self,
|
|
@@ -455,7 +501,7 @@ class ClientBase(ABC):
|
|
|
455
501
|
|
|
456
502
|
def _make_request(
|
|
457
503
|
self,
|
|
458
|
-
|
|
504
|
+
|
|
459
505
|
path: str,
|
|
460
506
|
*,
|
|
461
507
|
params: dict[str, Any] | None = None,
|
|
@@ -469,28 +515,49 @@ class ClientBase(ABC):
|
|
|
469
515
|
# Validate request parameters
|
|
470
516
|
self._validate_request_params(json_data, json_model)
|
|
471
517
|
|
|
472
|
-
# Build
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
518
|
+
# Build the transport request once and drive it through the
|
|
519
|
+
# middleware chain. The chain's terminal handler is transport-
|
|
520
|
+
# specific; middleware itself is transport-agnostic.
|
|
521
|
+
request = self._build_client_request(
|
|
522
|
+
path, json_data, json_model, headers_model
|
|
476
523
|
)
|
|
524
|
+
terminal = self._make_terminal(params)
|
|
477
525
|
|
|
478
526
|
# Execute request with timing
|
|
479
527
|
start_time = time.time()
|
|
480
528
|
|
|
481
529
|
try:
|
|
482
|
-
|
|
483
|
-
|
|
530
|
+
client_response = self.middleware_chain.execute(request, terminal)
|
|
531
|
+
# Body / status / headers are read from the structural
|
|
532
|
+
# response so any middleware transforms apply; `raw` is used
|
|
533
|
+
# only as the metadata sidecar (preserves `.request` /
|
|
534
|
+
# `.extensions` / `.history` from the real wire response when
|
|
535
|
+
# available, fallback to the synthetic for custom transports).
|
|
536
|
+
parsed_response = httpx.Response(
|
|
537
|
+
status_code=client_response.status,
|
|
538
|
+
headers=client_response.headers,
|
|
539
|
+
content=client_response.body,
|
|
540
|
+
)
|
|
541
|
+
metadata = TransportMetadata(
|
|
542
|
+
status_code=client_response.status,
|
|
543
|
+
headers=client_response.headers,
|
|
544
|
+
timing=time.time() - start_time,
|
|
545
|
+
raw_response=client_response.raw or parsed_response,
|
|
546
|
+
)
|
|
484
547
|
|
|
485
548
|
# Handle error responses
|
|
486
|
-
self._handle_error_response(
|
|
549
|
+
self._handle_error_response(
|
|
550
|
+
parsed_response, metadata, error_model=error_model
|
|
551
|
+
)
|
|
487
552
|
|
|
488
553
|
# Validate and return response
|
|
489
554
|
if response_model is not None:
|
|
490
|
-
return self._validate_response_model(
|
|
555
|
+
return self._validate_response_model(
|
|
556
|
+
parsed_response, response_model, metadata
|
|
557
|
+
)
|
|
491
558
|
else:
|
|
492
559
|
# No response_model provided - parse JSON into dict
|
|
493
|
-
json_response = self._parse_json_response(
|
|
560
|
+
json_response = self._parse_json_response(parsed_response)
|
|
494
561
|
return ApiResponse(json_response, metadata)
|
|
495
562
|
|
|
496
563
|
except httpx.TimeoutException as e:
|
|
@@ -500,7 +567,7 @@ class ClientBase(ABC):
|
|
|
500
567
|
|
|
501
568
|
def _make_sse_request(
|
|
502
569
|
self,
|
|
503
|
-
|
|
570
|
+
|
|
504
571
|
path: str,
|
|
505
572
|
*,
|
|
506
573
|
params: dict[str, Any] | None = None,
|
|
@@ -526,9 +593,10 @@ class ClientBase(ABC):
|
|
|
526
593
|
self._validate_request_params(json_data, json_model)
|
|
527
594
|
|
|
528
595
|
url = f"{self.base_url}/{path.lstrip('/')}"
|
|
529
|
-
|
|
530
|
-
|
|
596
|
+
client_request = self._build_client_request(
|
|
597
|
+
path, json_data, json_model, headers_model
|
|
531
598
|
)
|
|
599
|
+
request = self._build_httpx_request(client_request, url, params)
|
|
532
600
|
request.headers["accept"] = "text/event-stream"
|
|
533
601
|
|
|
534
602
|
start_time = time.time()
|
|
@@ -587,7 +655,7 @@ class AsyncClientBase(ABC):
|
|
|
587
655
|
headers: dict[str, str] | None = None,
|
|
588
656
|
middleware: list[AsyncMiddleware] | None = None,
|
|
589
657
|
auth: AuthHandler | httpx.Auth | None = None,
|
|
590
|
-
client: httpx.AsyncClient | None = None,
|
|
658
|
+
client: AsyncClient | httpx.AsyncClient | None = None,
|
|
591
659
|
) -> None:
|
|
592
660
|
self.base_url = base_url.rstrip("/")
|
|
593
661
|
self.middleware_chain = AsyncMiddlewareChain(middleware or [])
|
|
@@ -691,7 +759,7 @@ class AsyncClientBase(ABC):
|
|
|
691
759
|
@overload
|
|
692
760
|
async def _make_request(
|
|
693
761
|
self,
|
|
694
|
-
|
|
762
|
+
|
|
695
763
|
path: str,
|
|
696
764
|
*,
|
|
697
765
|
params: dict[str, Any] | None = None,
|
|
@@ -703,7 +771,7 @@ class AsyncClientBase(ABC):
|
|
|
703
771
|
@overload
|
|
704
772
|
async def _make_request(
|
|
705
773
|
self,
|
|
706
|
-
|
|
774
|
+
|
|
707
775
|
path: str,
|
|
708
776
|
*,
|
|
709
777
|
params: dict[str, Any] | None = None,
|
|
@@ -716,7 +784,7 @@ class AsyncClientBase(ABC):
|
|
|
716
784
|
@overload
|
|
717
785
|
async def _make_request(
|
|
718
786
|
self,
|
|
719
|
-
|
|
787
|
+
|
|
720
788
|
path: str,
|
|
721
789
|
*,
|
|
722
790
|
params: dict[str, Any] | None = None,
|
|
@@ -729,7 +797,7 @@ class AsyncClientBase(ABC):
|
|
|
729
797
|
@overload
|
|
730
798
|
async def _make_request(
|
|
731
799
|
self,
|
|
732
|
-
|
|
800
|
+
|
|
733
801
|
path: str,
|
|
734
802
|
*,
|
|
735
803
|
params: dict[str, Any] | None = None,
|
|
@@ -741,7 +809,7 @@ class AsyncClientBase(ABC):
|
|
|
741
809
|
@overload
|
|
742
810
|
async def _make_request(
|
|
743
811
|
self,
|
|
744
|
-
|
|
812
|
+
|
|
745
813
|
path: str,
|
|
746
814
|
*,
|
|
747
815
|
params: dict[str, Any] | None = None,
|
|
@@ -754,7 +822,7 @@ class AsyncClientBase(ABC):
|
|
|
754
822
|
@overload
|
|
755
823
|
async def _make_request(
|
|
756
824
|
self,
|
|
757
|
-
|
|
825
|
+
|
|
758
826
|
path: str,
|
|
759
827
|
*,
|
|
760
828
|
params: dict[str, Any] | None = None,
|
|
@@ -766,7 +834,7 @@ class AsyncClientBase(ABC):
|
|
|
766
834
|
|
|
767
835
|
def _validate_request_params(
|
|
768
836
|
self,
|
|
769
|
-
json_data:
|
|
837
|
+
json_data: Any | None,
|
|
770
838
|
json_model: BaseModel | None,
|
|
771
839
|
) -> None:
|
|
772
840
|
"""Validate request parameters for conflicts."""
|
|
@@ -847,58 +915,99 @@ class AsyncClientBase(ABC):
|
|
|
847
915
|
|
|
848
916
|
return headers
|
|
849
917
|
|
|
850
|
-
def
|
|
918
|
+
def _build_client_request(
|
|
851
919
|
self,
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
params: dict[str, Any] | None,
|
|
855
|
-
json_data: dict[str, Any] | None,
|
|
920
|
+
path: str,
|
|
921
|
+
json_data: Any | None,
|
|
856
922
|
json_model: BaseModel | None,
|
|
857
923
|
headers_model: BaseModel | None,
|
|
858
|
-
) ->
|
|
859
|
-
"""Build
|
|
924
|
+
) -> Request:
|
|
925
|
+
"""Build ReflectAPI transport request object."""
|
|
860
926
|
if json_model is not None:
|
|
861
|
-
# Serialize Pydantic model
|
|
862
927
|
content, base_headers = self._serialize_request_body(json_model)
|
|
863
928
|
headers = self._build_headers(base_headers, headers_model)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
url=url,
|
|
868
|
-
params=params,
|
|
869
|
-
content=content,
|
|
870
|
-
headers=headers,
|
|
871
|
-
)
|
|
872
|
-
else:
|
|
873
|
-
# Handle JSON data with Option types
|
|
874
|
-
if json_data is not None:
|
|
875
|
-
# Only serialize Option types for dictionaries (complex types)
|
|
876
|
-
# Primitive types (int, str, bool, etc.) should be passed directly
|
|
877
|
-
if isinstance(json_data, dict):
|
|
878
|
-
processed_json_data = serialize_option_dict(json_data)
|
|
879
|
-
else:
|
|
880
|
-
# Primitive types - pass through directly
|
|
881
|
-
processed_json_data = json_data
|
|
929
|
+
elif json_data is not None:
|
|
930
|
+
if isinstance(json_data, dict):
|
|
931
|
+
processed_json_data = serialize_option_dict(json_data)
|
|
882
932
|
else:
|
|
883
933
|
processed_json_data = json_data
|
|
884
|
-
|
|
885
|
-
|
|
934
|
+
content = json.dumps(
|
|
935
|
+
processed_json_data,
|
|
936
|
+
default=_json_serializer,
|
|
937
|
+
separators=(",", ":"),
|
|
938
|
+
).encode("utf-8")
|
|
939
|
+
headers = self._build_headers(
|
|
940
|
+
{"Content-Type": "application/json"}, headers_model
|
|
941
|
+
)
|
|
942
|
+
else:
|
|
943
|
+
content = b""
|
|
886
944
|
headers = self._build_headers({}, headers_model)
|
|
887
945
|
|
|
888
|
-
|
|
889
|
-
method=method,
|
|
890
|
-
url=url,
|
|
891
|
-
params=params,
|
|
892
|
-
json=processed_json_data,
|
|
893
|
-
headers=headers if headers else None,
|
|
894
|
-
)
|
|
946
|
+
return Request(path=path, headers=headers, body=content)
|
|
895
947
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
948
|
+
def _build_httpx_request(
|
|
949
|
+
self,
|
|
950
|
+
request: Request,
|
|
951
|
+
url: str,
|
|
952
|
+
params: dict[str, Any] | None,
|
|
953
|
+
) -> httpx.Request:
|
|
954
|
+
"""Materialise an httpx.Request from a transport Request.
|
|
955
|
+
|
|
956
|
+
Method is always POST by design.
|
|
957
|
+
"""
|
|
958
|
+
request_kwargs: dict[str, Any] = {
|
|
959
|
+
"method": "POST",
|
|
960
|
+
"url": url,
|
|
961
|
+
"params": params,
|
|
962
|
+
"headers": request.headers if request.headers else None,
|
|
963
|
+
}
|
|
964
|
+
if request.body:
|
|
965
|
+
request_kwargs["content"] = request.body
|
|
966
|
+
return self._client.build_request(**request_kwargs)
|
|
967
|
+
|
|
968
|
+
def _make_terminal(
|
|
969
|
+
self,
|
|
970
|
+
params: dict[str, Any] | None,
|
|
971
|
+
) -> Callable[[Request], Awaitable[Response]]:
|
|
972
|
+
"""Return the chain-terminal handler for the active transport.
|
|
973
|
+
|
|
974
|
+
For an ``httpx.AsyncClient``, the terminal lifts the Request into
|
|
975
|
+
an ``httpx.Request``, sends it, and surfaces the real
|
|
976
|
+
``httpx.Response`` via ``Response.raw``.
|
|
977
|
+
|
|
978
|
+
For a custom :class:`AsyncClient`, the terminal accepts either a
|
|
979
|
+
:class:`Response` or an ``httpx.Response``; the latter is a
|
|
980
|
+
*transitional* backward-compat shim for adapters written against
|
|
981
|
+
the previous runtime.
|
|
982
|
+
"""
|
|
983
|
+
if isinstance(self._client, httpx.AsyncClient):
|
|
984
|
+
base_url = self.base_url
|
|
985
|
+
|
|
986
|
+
async def terminal(req: Request) -> Response:
|
|
987
|
+
url = f"{base_url}/{req.path.lstrip('/')}"
|
|
988
|
+
httpx_req = self._build_httpx_request(req, url, params)
|
|
989
|
+
httpx_resp = await self._client.send(httpx_req)
|
|
990
|
+
return Response(
|
|
991
|
+
status=httpx_resp.status_code,
|
|
992
|
+
headers=httpx_resp.headers,
|
|
993
|
+
body=httpx_resp.content,
|
|
994
|
+
raw=httpx_resp,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
return terminal
|
|
998
|
+
|
|
999
|
+
async def terminal(req: Request) -> Response:
|
|
1000
|
+
response = await self._client.request(req)
|
|
1001
|
+
if isinstance(response, httpx.Response):
|
|
1002
|
+
return Response(
|
|
1003
|
+
status=response.status_code,
|
|
1004
|
+
headers=response.headers,
|
|
1005
|
+
body=response.content,
|
|
1006
|
+
raw=response,
|
|
1007
|
+
)
|
|
1008
|
+
return response
|
|
1009
|
+
|
|
1010
|
+
return terminal
|
|
902
1011
|
|
|
903
1012
|
def _handle_error_response(
|
|
904
1013
|
self,
|
|
@@ -969,7 +1078,7 @@ class AsyncClientBase(ABC):
|
|
|
969
1078
|
|
|
970
1079
|
async def _make_request(
|
|
971
1080
|
self,
|
|
972
|
-
|
|
1081
|
+
|
|
973
1082
|
path: str,
|
|
974
1083
|
*,
|
|
975
1084
|
params: dict[str, Any] | None = None,
|
|
@@ -983,27 +1092,48 @@ class AsyncClientBase(ABC):
|
|
|
983
1092
|
# Validate request parameters
|
|
984
1093
|
self._validate_request_params(json_data, json_model)
|
|
985
1094
|
|
|
986
|
-
# Build
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1095
|
+
# Build the transport request once and drive it through the
|
|
1096
|
+
# middleware chain. The chain's terminal handler is transport-
|
|
1097
|
+
# specific; middleware itself is transport-agnostic.
|
|
1098
|
+
request = self._build_client_request(
|
|
1099
|
+
path, json_data, json_model, headers_model
|
|
990
1100
|
)
|
|
1101
|
+
terminal = self._make_terminal(params)
|
|
991
1102
|
|
|
992
1103
|
# Execute request with timing
|
|
993
1104
|
start_time = time.time()
|
|
994
1105
|
|
|
995
1106
|
try:
|
|
996
|
-
|
|
997
|
-
|
|
1107
|
+
client_response = await self.middleware_chain.execute(request, terminal)
|
|
1108
|
+
# Body / status / headers are read from the structural
|
|
1109
|
+
# response so any middleware transforms apply; `raw` is used
|
|
1110
|
+
# only as the metadata sidecar (preserves `.request` /
|
|
1111
|
+
# `.extensions` / `.history` from the real wire response when
|
|
1112
|
+
# available, fallback to the synthetic for custom transports).
|
|
1113
|
+
parsed_response = httpx.Response(
|
|
1114
|
+
status_code=client_response.status,
|
|
1115
|
+
headers=client_response.headers,
|
|
1116
|
+
content=client_response.body,
|
|
1117
|
+
)
|
|
1118
|
+
metadata = TransportMetadata(
|
|
1119
|
+
status_code=client_response.status,
|
|
1120
|
+
headers=client_response.headers,
|
|
1121
|
+
timing=time.time() - start_time,
|
|
1122
|
+
raw_response=client_response.raw or parsed_response,
|
|
1123
|
+
)
|
|
998
1124
|
|
|
999
1125
|
# Handle error responses
|
|
1000
|
-
self._handle_error_response(
|
|
1126
|
+
self._handle_error_response(
|
|
1127
|
+
parsed_response, metadata, error_model=error_model
|
|
1128
|
+
)
|
|
1001
1129
|
|
|
1002
1130
|
# Validate and return response
|
|
1003
1131
|
if response_model is not None:
|
|
1004
|
-
return self._validate_response_model(
|
|
1132
|
+
return self._validate_response_model(
|
|
1133
|
+
parsed_response, response_model, metadata
|
|
1134
|
+
)
|
|
1005
1135
|
else:
|
|
1006
|
-
json_response = self._parse_json_response(
|
|
1136
|
+
json_response = self._parse_json_response(parsed_response)
|
|
1007
1137
|
return ApiResponse(json_response, metadata)
|
|
1008
1138
|
|
|
1009
1139
|
except httpx.TimeoutException as e:
|
|
@@ -1013,7 +1143,7 @@ class AsyncClientBase(ABC):
|
|
|
1013
1143
|
|
|
1014
1144
|
async def _make_sse_request(
|
|
1015
1145
|
self,
|
|
1016
|
-
|
|
1146
|
+
|
|
1017
1147
|
path: str,
|
|
1018
1148
|
*,
|
|
1019
1149
|
params: dict[str, Any] | None = None,
|
|
@@ -1039,9 +1169,10 @@ class AsyncClientBase(ABC):
|
|
|
1039
1169
|
self._validate_request_params(json_data, json_model)
|
|
1040
1170
|
|
|
1041
1171
|
url = f"{self.base_url}/{path.lstrip('/')}"
|
|
1042
|
-
|
|
1043
|
-
|
|
1172
|
+
client_request = self._build_client_request(
|
|
1173
|
+
path, json_data, json_model, headers_model
|
|
1044
1174
|
)
|
|
1175
|
+
request = self._build_httpx_request(client_request, url, params)
|
|
1045
1176
|
request.headers["accept"] = "text/event-stream"
|
|
1046
1177
|
|
|
1047
1178
|
start_time = time.time()
|