sg-python-api 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/METADATA +21 -24
- sg_python_api-0.2.0.dist-info/RECORD +58 -0
- shotgun_api3/__init__.py +4 -0
- shotgun_api3/_async_client.py +347 -281
- shotgun_api3/_async_driver.py +25 -55
- shotgun_api3/_async_httpx2.py +92 -59
- shotgun_api3/_async_transport.py +38 -3
- shotgun_api3/_capabilities.py +4 -3
- shotgun_api3/_client.py +296 -422
- shotgun_api3/_config.py +245 -120
- shotgun_api3/_connections.py +144 -0
- shotgun_api3/_crud.py +25 -250
- shotgun_api3/_driver.py +29 -54
- shotgun_api3/_execution.py +1 -3
- shotgun_api3/_files.py +193 -21
- shotgun_api3/_methods.py +59 -411
- shotgun_api3/_protocol.py +27 -53
- shotgun_api3/_transport.py +82 -407
- shotgun_api3/_version.py +7 -2
- shotgun_api3/lib/mockgun/__init__.py +20 -7
- shotgun_api3/lib/mockgun/_activity.py +510 -0
- shotgun_api3/lib/mockgun/_async_client.py +196 -0
- shotgun_api3/lib/mockgun/_dates.py +164 -0
- shotgun_api3/lib/mockgun/_errors.py +18 -0
- shotgun_api3/lib/mockgun/_events.py +1031 -0
- shotgun_api3/lib/mockgun/_follows.py +217 -0
- shotgun_api3/lib/mockgun/_handlers.py +558 -0
- shotgun_api3/lib/mockgun/_links.py +440 -0
- shotgun_api3/lib/mockgun/_new_client.py +92 -0
- shotgun_api3/lib/mockgun/_presets.py +492 -0
- shotgun_api3/lib/mockgun/_query.py +2163 -0
- shotgun_api3/lib/mockgun/_retirement.py +368 -0
- shotgun_api3/lib/mockgun/_schema.py +172 -0
- shotgun_api3/lib/mockgun/_schema_writes.py +425 -0
- shotgun_api3/lib/mockgun/_search.py +378 -0
- shotgun_api3/lib/mockgun/_server.py +169 -0
- shotgun_api3/lib/mockgun/_sessions.py +206 -0
- shotgun_api3/lib/mockgun/_site.py +150 -0
- shotgun_api3/lib/mockgun/_site_data.py +584 -0
- shotgun_api3/lib/mockgun/_storage.py +208 -0
- shotgun_api3/lib/mockgun/_store.py +1446 -0
- shotgun_api3/lib/mockgun/_summaries.py +964 -0
- shotgun_api3/lib/mockgun/_sync_client.py +190 -0
- shotgun_api3/lib/mockgun/_threads.py +226 -0
- shotgun_api3/lib/mockgun/_thumbnails.py +146 -0
- shotgun_api3/lib/mockgun/_transport.py +145 -0
- shotgun_api3/lib/mockgun/_uploads.py +417 -0
- shotgun_api3/lib/mockgun/_wire.py +120 -0
- shotgun_api3/lib/mockgun/mockgun.py +14 -0
- shotgun_api3/lib/sgtimezone.py +24 -1
- sg_python_api-0.1.2.dist-info/RECORD +0 -31
- shotgun_api3/_signatures.py +0 -53
- shotgun_api3/lib/mockgun/_async.py +0 -315
- shotgun_api3/lib/mockgun/_mockgun.py +0 -1666
- {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/WHEEL +0 -0
- {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
shotgun_api3/_transport.py
CHANGED
|
@@ -1,36 +1,23 @@
|
|
|
1
1
|
"""Transport vocabulary and HTTP backend implementations."""
|
|
2
2
|
|
|
3
|
-
# Same-file collaboration follows module-boundary privacy.
|
|
4
|
-
# pyright: reportPrivateUsage=false
|
|
5
|
-
|
|
6
3
|
from __future__ import annotations
|
|
7
4
|
|
|
8
|
-
import contextvars
|
|
9
|
-
import socket
|
|
10
5
|
import ssl
|
|
11
|
-
import sys
|
|
12
6
|
import threading
|
|
13
7
|
import time
|
|
14
8
|
from collections.abc import Callable, Iterator, Mapping
|
|
15
|
-
from
|
|
16
|
-
from dataclasses import dataclass, field, replace
|
|
9
|
+
from dataclasses import dataclass, field
|
|
17
10
|
from types import MappingProxyType
|
|
18
|
-
from typing import
|
|
11
|
+
from typing import Any, Protocol, cast
|
|
19
12
|
|
|
20
13
|
import urllib3
|
|
21
|
-
import urllib3.connectionpool
|
|
22
14
|
import urllib3.exceptions
|
|
23
15
|
from urllib3.util.ssl_match_hostname import CertificateError
|
|
24
16
|
|
|
17
|
+
from ._connections import connection_pool_classes, keepalive_socket_options
|
|
25
18
|
from ._errors import ShotgunError
|
|
26
19
|
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from urllib3._base_connection import BaseHTTPConnection
|
|
29
|
-
|
|
30
20
|
_CERTIFICATE_ERRORS = (ssl.SSLCertVerificationError, CertificateError)
|
|
31
|
-
_CURRENT_ABORT: contextvars.ContextVar[tuple[_AbortRegistry, _AbortHandle] | None] = (
|
|
32
|
-
contextvars.ContextVar('_CURRENT_ABORT', default=None)
|
|
33
|
-
)
|
|
34
21
|
|
|
35
22
|
|
|
36
23
|
class CaseInsensitiveHeaders(Mapping[str, str]):
|
|
@@ -59,7 +46,11 @@ class CaseInsensitiveHeaders(Mapping[str, str]):
|
|
|
59
46
|
self._lookup = MappingProxyType(values)
|
|
60
47
|
|
|
61
48
|
def __getitem__(self, key: str) -> str:
|
|
62
|
-
"""Return one header value by case-insensitive name.
|
|
49
|
+
"""Return one header value by case-insensitive name.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
KeyError: If no header matches the requested name.
|
|
53
|
+
"""
|
|
63
54
|
return self._lookup[key.casefold()]
|
|
64
55
|
|
|
65
56
|
def __iter__(self) -> Iterator[str]:
|
|
@@ -123,7 +114,14 @@ class StreamingResponse:
|
|
|
123
114
|
self._close_lock = threading.Lock()
|
|
124
115
|
|
|
125
116
|
def close(self) -> None:
|
|
126
|
-
"""
|
|
117
|
+
"""Run the response cleanup callback at most once.
|
|
118
|
+
|
|
119
|
+
The callback is cleared before invocation, so a failed cleanup is not
|
|
120
|
+
retried by later calls.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
Exception: If the supplied cleanup callback fails.
|
|
124
|
+
"""
|
|
127
125
|
with self._close_lock:
|
|
128
126
|
release = self._release
|
|
129
127
|
self._release = None
|
|
@@ -170,29 +168,23 @@ class StreamingResponse:
|
|
|
170
168
|
|
|
171
169
|
@dataclass(frozen=True)
|
|
172
170
|
class TimeoutConfig:
|
|
173
|
-
"""Connect
|
|
171
|
+
"""Connect and read timeout limits.
|
|
174
172
|
|
|
175
|
-
|
|
173
|
+
A `None` field has no timeout limit.
|
|
176
174
|
|
|
177
175
|
Attributes:
|
|
178
176
|
connect: Connection-establishment timeout in seconds.
|
|
179
177
|
read: Socket-read timeout in seconds.
|
|
180
|
-
total: Total attempt timeout through body consumption.
|
|
181
178
|
"""
|
|
182
179
|
|
|
183
180
|
connect: float | None
|
|
184
181
|
read: float | None
|
|
185
|
-
total: float | None = None
|
|
186
182
|
|
|
187
183
|
|
|
188
184
|
class TransportError(ShotgunError):
|
|
189
185
|
"""Base exception for transport-level failures."""
|
|
190
186
|
|
|
191
187
|
|
|
192
|
-
class AbortedError(TransportError):
|
|
193
|
-
"""Raised when a request is deliberately aborted via its token."""
|
|
194
|
-
|
|
195
|
-
|
|
196
188
|
class ConnectError(TransportError):
|
|
197
189
|
"""Raised when establishing a connection fails before transmission."""
|
|
198
190
|
|
|
@@ -223,7 +215,6 @@ class TransportRequest:
|
|
|
223
215
|
headers: Request headers.
|
|
224
216
|
body: Buffered or streaming request body.
|
|
225
217
|
timeout: Timeout limits for this request.
|
|
226
|
-
token: Unique token used for cancellation.
|
|
227
218
|
"""
|
|
228
219
|
|
|
229
220
|
method: str
|
|
@@ -231,7 +222,6 @@ class TransportRequest:
|
|
|
231
222
|
headers: dict[str, str]
|
|
232
223
|
body: bytes | Iterator[bytes] | None
|
|
233
224
|
timeout: TimeoutConfig
|
|
234
|
-
token: str
|
|
235
225
|
|
|
236
226
|
|
|
237
227
|
@dataclass(frozen=True, init=False)
|
|
@@ -282,7 +272,14 @@ class TransportResponse:
|
|
|
282
272
|
object.__setattr__(self, '_on_close', on_close)
|
|
283
273
|
|
|
284
274
|
def close(self) -> None:
|
|
285
|
-
"""
|
|
275
|
+
"""Run the response cleanup callback at most once.
|
|
276
|
+
|
|
277
|
+
The callback is cleared before invocation, so a failed cleanup is not
|
|
278
|
+
retried by later calls.
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
Exception: If the supplied cleanup callback fails.
|
|
282
|
+
"""
|
|
286
283
|
with self._close_lock:
|
|
287
284
|
callback = self._on_close
|
|
288
285
|
object.__setattr__(self, '_on_close', None)
|
|
@@ -293,14 +290,6 @@ class TransportResponse:
|
|
|
293
290
|
class Transport(Protocol):
|
|
294
291
|
"""HTTP backend operations required by the driver."""
|
|
295
292
|
|
|
296
|
-
def abort(self, token: str) -> None:
|
|
297
|
-
"""Abort the request identified by a token.
|
|
298
|
-
|
|
299
|
-
Args:
|
|
300
|
-
token: Unique request token.
|
|
301
|
-
"""
|
|
302
|
-
...
|
|
303
|
-
|
|
304
293
|
def close(self) -> None:
|
|
305
294
|
"""Release resources owned by the transport."""
|
|
306
295
|
...
|
|
@@ -334,146 +323,6 @@ class Transport(Protocol):
|
|
|
334
323
|
...
|
|
335
324
|
|
|
336
325
|
|
|
337
|
-
class _AbortableConnection(Protocol):
|
|
338
|
-
"""Socket operations required to abort an urllib3 connection."""
|
|
339
|
-
|
|
340
|
-
sock: socket.socket | ssl.SSLSocket | None
|
|
341
|
-
|
|
342
|
-
def close(self) -> None:
|
|
343
|
-
"""Close the connection and prevent its socket from being reused."""
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
@dataclass(eq=False)
|
|
347
|
-
class _AbortHandle:
|
|
348
|
-
"""Track one request registration independently of token reuse."""
|
|
349
|
-
|
|
350
|
-
token: str
|
|
351
|
-
generation: int
|
|
352
|
-
aborted: bool = False
|
|
353
|
-
connection: _AbortableConnection | None = None
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
class _AbortRegistry:
|
|
357
|
-
"""Track live connections and aborted requests under a lock."""
|
|
358
|
-
|
|
359
|
-
def __init__(self) -> None:
|
|
360
|
-
"""Initialize an empty transport-abort registry."""
|
|
361
|
-
self._active: dict[str, dict[int, _AbortHandle]] = {}
|
|
362
|
-
self._generation = 0
|
|
363
|
-
self._lock = threading.Lock()
|
|
364
|
-
|
|
365
|
-
def _abort(self, token: str) -> None:
|
|
366
|
-
"""Abort every active request registered for `token`."""
|
|
367
|
-
with self._lock:
|
|
368
|
-
connections: list[_AbortableConnection] = []
|
|
369
|
-
for handle in self._active.get(token, {}).values():
|
|
370
|
-
handle.aborted = True
|
|
371
|
-
if handle.connection is not None:
|
|
372
|
-
connections.append(handle.connection)
|
|
373
|
-
|
|
374
|
-
for connection in connections:
|
|
375
|
-
live_socket = connection.sock
|
|
376
|
-
if live_socket is not None:
|
|
377
|
-
with suppress(OSError):
|
|
378
|
-
live_socket.shutdown(socket.SHUT_RDWR)
|
|
379
|
-
if sys.platform == 'win32':
|
|
380
|
-
# Buffered readers defer close on Windows, leaving recv blocked.
|
|
381
|
-
# Transfer ownership so later reader closes cannot close a reused fd.
|
|
382
|
-
with suppress(OSError):
|
|
383
|
-
descriptor = live_socket.detach()
|
|
384
|
-
if descriptor != -1:
|
|
385
|
-
socket.close(descriptor)
|
|
386
|
-
connection.close()
|
|
387
|
-
|
|
388
|
-
def _attach(
|
|
389
|
-
self,
|
|
390
|
-
handle: _AbortHandle,
|
|
391
|
-
connection: _AbortableConnection,
|
|
392
|
-
) -> bool:
|
|
393
|
-
"""Attach a checked-out connection and report an earlier abort."""
|
|
394
|
-
with self._lock:
|
|
395
|
-
active = self._active.get(handle.token, {}).get(handle.generation)
|
|
396
|
-
if active is handle:
|
|
397
|
-
handle.connection = connection
|
|
398
|
-
return handle.aborted
|
|
399
|
-
|
|
400
|
-
def _begin(self, token: str) -> _AbortHandle:
|
|
401
|
-
"""Begin tracking a request before it checks out a connection."""
|
|
402
|
-
with self._lock:
|
|
403
|
-
self._generation += 1
|
|
404
|
-
handle = _AbortHandle(token=token, generation=self._generation)
|
|
405
|
-
self._active.setdefault(token, {})[handle.generation] = handle
|
|
406
|
-
return handle
|
|
407
|
-
|
|
408
|
-
def _detach(self, handle: _AbortHandle) -> bool:
|
|
409
|
-
"""Detach a connection before urllib3 can return it to the pool."""
|
|
410
|
-
with self._lock:
|
|
411
|
-
handle.connection = None
|
|
412
|
-
return handle.aborted
|
|
413
|
-
|
|
414
|
-
def _release(self, handle: _AbortHandle) -> None:
|
|
415
|
-
"""Release exactly one request generation."""
|
|
416
|
-
with self._lock:
|
|
417
|
-
active = self._active.get(handle.token)
|
|
418
|
-
if active is not None and active.get(handle.generation) is handle:
|
|
419
|
-
active.pop(handle.generation)
|
|
420
|
-
if not active:
|
|
421
|
-
self._active.pop(handle.token)
|
|
422
|
-
handle.connection = None
|
|
423
|
-
|
|
424
|
-
def _was_aborted(self, handle: _AbortHandle) -> bool:
|
|
425
|
-
"""Return whether a particular request was aborted."""
|
|
426
|
-
with self._lock:
|
|
427
|
-
return handle.aborted
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
@dataclass(frozen=True)
|
|
431
|
-
class _StartedUrllib3Request:
|
|
432
|
-
"""Hold request-start state whose lifetime remains caller-owned."""
|
|
433
|
-
|
|
434
|
-
response: urllib3.response.HTTPResponse
|
|
435
|
-
handle: _AbortHandle
|
|
436
|
-
started: float
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
class _HTTPPool(urllib3.connectionpool.HTTPConnectionPool):
|
|
440
|
-
"""HTTP pool that aligns abort registration with connection ownership."""
|
|
441
|
-
|
|
442
|
-
def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection:
|
|
443
|
-
"""Attach a request handle when a connection is checked out."""
|
|
444
|
-
return _attach_checked_out_connection(super()._get_conn(timeout))
|
|
445
|
-
|
|
446
|
-
def _put_conn(self, conn: BaseHTTPConnection | None) -> None:
|
|
447
|
-
"""Detach a request handle before returning a connection."""
|
|
448
|
-
super()._put_conn(_detach_returned_connection(conn))
|
|
449
|
-
|
|
450
|
-
def _validate_conn(self, conn: BaseHTTPConnection) -> None:
|
|
451
|
-
"""Connect while checking abort state before and after socket creation."""
|
|
452
|
-
_raise_if_current_aborted(conn)
|
|
453
|
-
super()._validate_conn(conn)
|
|
454
|
-
if conn.is_closed:
|
|
455
|
-
conn.connect()
|
|
456
|
-
_raise_if_current_aborted(conn)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
class _HTTPSPool(urllib3.connectionpool.HTTPSConnectionPool):
|
|
460
|
-
"""HTTPS pool that aligns abort registration with connection ownership."""
|
|
461
|
-
|
|
462
|
-
def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection:
|
|
463
|
-
"""Attach a request handle when a connection is checked out."""
|
|
464
|
-
return _attach_checked_out_connection(super()._get_conn(timeout))
|
|
465
|
-
|
|
466
|
-
def _put_conn(self, conn: BaseHTTPConnection | None) -> None:
|
|
467
|
-
"""Detach a request handle before returning a connection."""
|
|
468
|
-
super()._put_conn(_detach_returned_connection(conn))
|
|
469
|
-
|
|
470
|
-
def _validate_conn(self, conn: BaseHTTPConnection) -> None:
|
|
471
|
-
"""Validate TLS while checking abort state around socket creation."""
|
|
472
|
-
_raise_if_current_aborted(conn)
|
|
473
|
-
super()._validate_conn(conn)
|
|
474
|
-
_raise_if_current_aborted(conn)
|
|
475
|
-
|
|
476
|
-
|
|
477
326
|
class Urllib3Transport:
|
|
478
327
|
"""Transport implementation on a urllib3 connection pool.
|
|
479
328
|
|
|
@@ -481,12 +330,16 @@ class Urllib3Transport:
|
|
|
481
330
|
proxy_info: Compatibility proxy metadata for a direct connection.
|
|
482
331
|
"""
|
|
483
332
|
|
|
484
|
-
def __init__(
|
|
333
|
+
def __init__( # noqa: PLR0913
|
|
485
334
|
self,
|
|
486
335
|
ca_certs: str | None = None,
|
|
487
336
|
proxy_url: str | None = None,
|
|
488
337
|
proxy_headers: dict[str, str] | None = None,
|
|
489
338
|
pool_maxsize: int = 10,
|
|
339
|
+
*,
|
|
340
|
+
max_connection_idle_secs: float | None = 60.0,
|
|
341
|
+
tcp_keepalive_timeout_secs: int | None = 60,
|
|
342
|
+
clock: Callable[[], float] = time.monotonic,
|
|
490
343
|
) -> None:
|
|
491
344
|
"""Initialize a urllib3 connection pool transport.
|
|
492
345
|
|
|
@@ -495,8 +348,13 @@ class Urllib3Transport:
|
|
|
495
348
|
proxy_url: Optional proxy URL.
|
|
496
349
|
proxy_headers: Optional headers sent to the proxy.
|
|
497
350
|
pool_maxsize: Maximum connections retained by the pool.
|
|
351
|
+
max_connection_idle_secs: Validated idle limit in seconds, or
|
|
352
|
+
`None` to disable expiry. Counted after body preload for
|
|
353
|
+
buffered responses, or after headers for streamed responses.
|
|
354
|
+
tcp_keepalive_timeout_secs: Validated nominal keepalive target in
|
|
355
|
+
seconds, or `None` to disable tuning.
|
|
356
|
+
clock: Monotonic clock returning seconds.
|
|
498
357
|
"""
|
|
499
|
-
self._abort_registry = _AbortRegistry()
|
|
500
358
|
common: dict[str, Any] = {'maxsize': pool_maxsize}
|
|
501
359
|
if ca_certs is not None:
|
|
502
360
|
common['ca_certs'] = ca_certs
|
|
@@ -508,71 +366,41 @@ class Urllib3Transport:
|
|
|
508
366
|
)
|
|
509
367
|
else:
|
|
510
368
|
self._pool = urllib3.PoolManager(**common)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
urllib3.connectionpool.HTTPConnectionPool
|
|
515
|
-
| urllib3.connectionpool.HTTPSConnectionPool
|
|
516
|
-
],
|
|
517
|
-
] = {
|
|
518
|
-
'http': _HTTPPool,
|
|
519
|
-
'https': _HTTPSPool,
|
|
520
|
-
}
|
|
521
|
-
self._pool.pool_classes_by_scheme = pool_classes
|
|
369
|
+
self._pool.pool_classes_by_scheme = connection_pool_classes(
|
|
370
|
+
max_connection_idle_secs, keepalive_socket_options(tcp_keepalive_timeout_secs), clock
|
|
371
|
+
)
|
|
522
372
|
|
|
523
373
|
def _start_request(
|
|
524
374
|
self,
|
|
525
375
|
request: TransportRequest,
|
|
526
376
|
*,
|
|
527
377
|
preload_content: bool,
|
|
528
|
-
) ->
|
|
529
|
-
"""Start one urllib3 request and
|
|
378
|
+
) -> urllib3.response.HTTPResponse:
|
|
379
|
+
"""Start one urllib3 request and return its response."""
|
|
530
380
|
timeout = urllib3.Timeout(
|
|
531
381
|
connect=request.timeout.connect,
|
|
532
382
|
read=request.timeout.read,
|
|
533
|
-
total=request.timeout.total,
|
|
534
383
|
)
|
|
535
|
-
handle = self._abort_registry._begin(request.token)
|
|
536
|
-
context_token = _CURRENT_ABORT.set((self._abort_registry, handle))
|
|
537
384
|
try:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
raise ProxyError(str(exc)) from exc
|
|
558
|
-
except Exception as exc:
|
|
559
|
-
raise _map_unexpected_request_error(
|
|
560
|
-
exc, self._abort_registry, handle, request.token
|
|
561
|
-
) from exc
|
|
562
|
-
except BaseException:
|
|
563
|
-
self._abort_registry._release(handle)
|
|
564
|
-
raise
|
|
565
|
-
finally:
|
|
566
|
-
_CURRENT_ABORT.reset(context_token)
|
|
567
|
-
return _StartedUrllib3Request(response=response, handle=handle, started=started)
|
|
568
|
-
|
|
569
|
-
def abort(self, token: str) -> None:
|
|
570
|
-
"""Abort the in-flight request identified by a token.
|
|
571
|
-
|
|
572
|
-
Args:
|
|
573
|
-
token: Unique request token.
|
|
574
|
-
"""
|
|
575
|
-
self._abort_registry._abort(token)
|
|
385
|
+
response = cast(
|
|
386
|
+
'urllib3.response.HTTPResponse',
|
|
387
|
+
self._pool.request(
|
|
388
|
+
request.method,
|
|
389
|
+
request.url,
|
|
390
|
+
body=request.body,
|
|
391
|
+
headers=request.headers,
|
|
392
|
+
timeout=timeout,
|
|
393
|
+
retries=False,
|
|
394
|
+
preload_content=preload_content,
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
except urllib3.exceptions.HTTPError as exc:
|
|
398
|
+
raise _map_urllib3_error(exc) from exc
|
|
399
|
+
except urllib3.exceptions.ProxySchemeUnsupported as exc:
|
|
400
|
+
raise ProxyError(str(exc)) from exc
|
|
401
|
+
except Exception as exc:
|
|
402
|
+
raise SendError(str(exc)) from exc
|
|
403
|
+
return response
|
|
576
404
|
|
|
577
405
|
def close(self) -> None:
|
|
578
406
|
"""Release pooled connections owned by the transport."""
|
|
@@ -603,16 +431,13 @@ class Urllib3Transport:
|
|
|
603
431
|
Raises:
|
|
604
432
|
TransportError: If urllib3 reports a transport failure.
|
|
605
433
|
"""
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
)
|
|
614
|
-
finally:
|
|
615
|
-
self._abort_registry._release(started.handle)
|
|
434
|
+
response = self._start_request(request, preload_content=True)
|
|
435
|
+
return TransportResponse(
|
|
436
|
+
status=response.status,
|
|
437
|
+
reason=response.reason or '',
|
|
438
|
+
headers=dict(response.headers),
|
|
439
|
+
content=response.data,
|
|
440
|
+
)
|
|
616
441
|
|
|
617
442
|
def send_streaming(self, request: TransportRequest) -> StreamingResponse:
|
|
618
443
|
"""Send one request and return its single-use streaming response.
|
|
@@ -627,26 +452,13 @@ class Urllib3Transport:
|
|
|
627
452
|
TransportError: If urllib3 reports a transport failure while
|
|
628
453
|
receiving response headers.
|
|
629
454
|
"""
|
|
630
|
-
|
|
631
|
-
response = started.response
|
|
632
|
-
|
|
633
|
-
deadline = (
|
|
634
|
-
None if request.timeout.total is None else started.started + request.timeout.total
|
|
635
|
-
)
|
|
455
|
+
response = self._start_request(request, preload_content=False)
|
|
636
456
|
|
|
637
457
|
def release() -> None:
|
|
638
|
-
_release_urllib3_stream(response
|
|
458
|
+
_release_urllib3_stream(response)
|
|
639
459
|
|
|
640
460
|
def stream(size: int) -> Iterator[bytes]:
|
|
641
|
-
|
|
642
|
-
deadline=deadline,
|
|
643
|
-
handle=started.handle,
|
|
644
|
-
read_timeout=request.timeout.read,
|
|
645
|
-
registry=self._abort_registry,
|
|
646
|
-
response=response,
|
|
647
|
-
token=request.token,
|
|
648
|
-
)
|
|
649
|
-
return _stream_urllib3_response(attempt, size)
|
|
461
|
+
return _stream_urllib3_response(response, size)
|
|
650
462
|
|
|
651
463
|
try:
|
|
652
464
|
return StreamingResponse(
|
|
@@ -661,48 +473,6 @@ class Urllib3Transport:
|
|
|
661
473
|
raise
|
|
662
474
|
|
|
663
475
|
|
|
664
|
-
@dataclass(frozen=True)
|
|
665
|
-
class _Urllib3StreamingAttempt:
|
|
666
|
-
"""Bundle state that remains live through streaming body consumption."""
|
|
667
|
-
|
|
668
|
-
deadline: float | None
|
|
669
|
-
handle: _AbortHandle
|
|
670
|
-
read_timeout: float | None
|
|
671
|
-
registry: _AbortRegistry
|
|
672
|
-
response: urllib3.response.HTTPResponse
|
|
673
|
-
token: str
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
def _attach_checked_out_connection(
|
|
677
|
-
connection: BaseHTTPConnection,
|
|
678
|
-
) -> BaseHTTPConnection:
|
|
679
|
-
"""Attach the active request handle to a checked-out connection."""
|
|
680
|
-
current = _CURRENT_ABORT.get()
|
|
681
|
-
if current is None:
|
|
682
|
-
return connection
|
|
683
|
-
registry, handle = current
|
|
684
|
-
abortable = cast('_AbortableConnection', connection)
|
|
685
|
-
if registry._attach(handle, abortable):
|
|
686
|
-
connection.close()
|
|
687
|
-
raise AbortedError(handle.token)
|
|
688
|
-
return connection
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
def _detach_returned_connection(
|
|
692
|
-
connection: BaseHTTPConnection | None,
|
|
693
|
-
) -> BaseHTTPConnection | None:
|
|
694
|
-
"""Detach the active handle and discard a connection lost to abort."""
|
|
695
|
-
current = _CURRENT_ABORT.get()
|
|
696
|
-
if current is None:
|
|
697
|
-
return connection
|
|
698
|
-
registry, handle = current
|
|
699
|
-
if not registry._detach(handle):
|
|
700
|
-
return connection
|
|
701
|
-
if connection is not None:
|
|
702
|
-
connection.close()
|
|
703
|
-
return None
|
|
704
|
-
|
|
705
|
-
|
|
706
476
|
def _is_certificate_failure(exc: BaseException) -> bool:
|
|
707
477
|
"""Return whether an exception contains certificate-verification evidence."""
|
|
708
478
|
if isinstance(exc, _CERTIFICATE_ERRORS):
|
|
@@ -711,18 +481,6 @@ def _is_certificate_failure(exc: BaseException) -> bool:
|
|
|
711
481
|
return any(isinstance(item, _CERTIFICATE_ERRORS) for item in nested)
|
|
712
482
|
|
|
713
483
|
|
|
714
|
-
def _map_unexpected_request_error(
|
|
715
|
-
exc: Exception,
|
|
716
|
-
registry: _AbortRegistry,
|
|
717
|
-
handle: _AbortHandle,
|
|
718
|
-
token: str,
|
|
719
|
-
) -> TransportError:
|
|
720
|
-
"""Translate an unexpected request failure into transport vocabulary."""
|
|
721
|
-
if registry._was_aborted(handle):
|
|
722
|
-
return AbortedError(token)
|
|
723
|
-
return SendError(str(exc))
|
|
724
|
-
|
|
725
|
-
|
|
726
484
|
def _map_urllib3_error(exc: urllib3.exceptions.HTTPError) -> TransportError:
|
|
727
485
|
"""Translate a urllib3 exception into the transport vocabulary."""
|
|
728
486
|
unwrapped: BaseException = exc
|
|
@@ -743,9 +501,7 @@ def _map_urllib3_error(exc: urllib3.exceptions.HTTPError) -> TransportError:
|
|
|
743
501
|
|
|
744
502
|
|
|
745
503
|
def _next_urllib3_stream_chunk(
|
|
746
|
-
attempt: _Urllib3StreamingAttempt,
|
|
747
504
|
chunks: Iterator[bytes],
|
|
748
|
-
total_limited: bool,
|
|
749
505
|
) -> bytes:
|
|
750
506
|
"""Read one chunk while translating every backend body failure."""
|
|
751
507
|
try:
|
|
@@ -753,38 +509,17 @@ def _next_urllib3_stream_chunk(
|
|
|
753
509
|
except StopIteration:
|
|
754
510
|
raise
|
|
755
511
|
except urllib3.exceptions.HTTPError as exc:
|
|
756
|
-
|
|
757
|
-
raise AbortedError(attempt.token) from exc
|
|
758
|
-
mapped = _map_urllib3_error(exc)
|
|
759
|
-
if total_limited and isinstance(mapped, ReadTimeoutError):
|
|
760
|
-
raise ReadTimeoutError('total streaming timeout exceeded') from exc
|
|
761
|
-
raise mapped from exc
|
|
512
|
+
raise _map_urllib3_error(exc) from exc
|
|
762
513
|
except urllib3.exceptions.ProxySchemeUnsupported as exc:
|
|
763
514
|
raise ProxyError(str(exc)) from exc
|
|
764
515
|
except Exception as exc:
|
|
765
|
-
if attempt.registry._was_aborted(attempt.handle):
|
|
766
|
-
raise AbortedError(attempt.token) from exc
|
|
767
516
|
raise SendError(str(exc)) from exc
|
|
768
517
|
|
|
769
518
|
|
|
770
|
-
def _raise_if_current_aborted(connection: BaseHTTPConnection) -> None:
|
|
771
|
-
"""Close a connection and raise when its active request was aborted."""
|
|
772
|
-
current = _CURRENT_ABORT.get()
|
|
773
|
-
if current is None:
|
|
774
|
-
return
|
|
775
|
-
registry, handle = current
|
|
776
|
-
if registry._was_aborted(handle):
|
|
777
|
-
connection.close()
|
|
778
|
-
raise AbortedError(handle.token)
|
|
779
|
-
|
|
780
|
-
|
|
781
519
|
def _release_urllib3_stream(
|
|
782
520
|
response: urllib3.response.HTTPResponse,
|
|
783
|
-
registry: _AbortRegistry,
|
|
784
|
-
handle: _AbortHandle,
|
|
785
521
|
) -> None:
|
|
786
|
-
"""
|
|
787
|
-
registry._release(handle)
|
|
522
|
+
"""Close a streaming response and return its owned connection."""
|
|
788
523
|
if response.connection is None:
|
|
789
524
|
return
|
|
790
525
|
try:
|
|
@@ -793,53 +528,17 @@ def _release_urllib3_stream(
|
|
|
793
528
|
response.release_conn()
|
|
794
529
|
|
|
795
530
|
|
|
796
|
-
def _set_stream_read_timeout(
|
|
797
|
-
attempt: _Urllib3StreamingAttempt,
|
|
798
|
-
) -> bool:
|
|
799
|
-
"""Apply the active timeout and report whether total is the limiter."""
|
|
800
|
-
if attempt.deadline is None:
|
|
801
|
-
return False
|
|
802
|
-
remaining = attempt.deadline - time.monotonic()
|
|
803
|
-
if remaining <= 0:
|
|
804
|
-
raise ReadTimeoutError('total streaming timeout exceeded')
|
|
805
|
-
total_limited = attempt.read_timeout is None or remaining <= attempt.read_timeout
|
|
806
|
-
connection = attempt.response.connection
|
|
807
|
-
if connection is None:
|
|
808
|
-
return total_limited
|
|
809
|
-
live_socket = connection.sock
|
|
810
|
-
if live_socket is None:
|
|
811
|
-
return total_limited
|
|
812
|
-
try:
|
|
813
|
-
live_socket.settimeout(
|
|
814
|
-
remaining if attempt.read_timeout is None else min(attempt.read_timeout, remaining)
|
|
815
|
-
)
|
|
816
|
-
except Exception as exc:
|
|
817
|
-
raise _map_unexpected_request_error(
|
|
818
|
-
exc, attempt.registry, attempt.handle, attempt.token
|
|
819
|
-
) from exc
|
|
820
|
-
return total_limited
|
|
821
|
-
|
|
822
|
-
|
|
823
531
|
def _stream_urllib3_response(
|
|
824
|
-
|
|
532
|
+
response: urllib3.response.HTTPResponse,
|
|
825
533
|
size: int,
|
|
826
534
|
) -> Iterator[bytes]:
|
|
827
|
-
"""Yield body chunks while mapping errors
|
|
828
|
-
chunks =
|
|
535
|
+
"""Yield body chunks while mapping backend errors."""
|
|
536
|
+
chunks = response.stream(size)
|
|
829
537
|
while True:
|
|
830
|
-
if attempt.registry._was_aborted(attempt.handle):
|
|
831
|
-
raise AbortedError(attempt.token)
|
|
832
|
-
total_limited = _set_stream_read_timeout(attempt)
|
|
833
|
-
context_token = _CURRENT_ABORT.set((attempt.registry, attempt.handle))
|
|
834
538
|
try:
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
return
|
|
839
|
-
finally:
|
|
840
|
-
_CURRENT_ABORT.reset(context_token)
|
|
841
|
-
if attempt.deadline is not None and time.monotonic() > attempt.deadline:
|
|
842
|
-
raise ReadTimeoutError('total streaming timeout exceeded')
|
|
539
|
+
chunk = _next_urllib3_stream_chunk(chunks)
|
|
540
|
+
except StopIteration:
|
|
541
|
+
return
|
|
843
542
|
yield chunk
|
|
844
543
|
|
|
845
544
|
|
|
@@ -858,6 +557,9 @@ def body_close(body: object) -> Callable[[], object] | None:
|
|
|
858
557
|
def content_length(headers: Mapping[str, str]) -> int | None:
|
|
859
558
|
"""Parse a nonnegative content length when present.
|
|
860
559
|
|
|
560
|
+
Args:
|
|
561
|
+
headers: Response headers whose names are matched case-insensitively.
|
|
562
|
+
|
|
861
563
|
Returns:
|
|
862
564
|
Parsed nonnegative length, or `None` when absent or invalid.
|
|
863
565
|
"""
|
|
@@ -872,30 +574,3 @@ def content_length(headers: Mapping[str, str]) -> int | None:
|
|
|
872
574
|
except ValueError:
|
|
873
575
|
return None
|
|
874
576
|
return length if length >= 0 else None
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
def narrow_total_timeout(
|
|
878
|
-
request: TransportRequest,
|
|
879
|
-
remaining: float | None,
|
|
880
|
-
) -> TransportRequest:
|
|
881
|
-
"""Restrict a request to its nonnegative remaining total timeout.
|
|
882
|
-
|
|
883
|
-
Args:
|
|
884
|
-
request: Original transport request.
|
|
885
|
-
remaining: Remaining total timeout in seconds, when limited.
|
|
886
|
-
|
|
887
|
-
Returns:
|
|
888
|
-
The original request when `remaining` is `None`. Otherwise, a
|
|
889
|
-
replacement whose total timeout is `remaining` clamped to zero and
|
|
890
|
-
capped by any existing total timeout. Headers and body are shared with
|
|
891
|
-
the original request.
|
|
892
|
-
"""
|
|
893
|
-
if remaining is None:
|
|
894
|
-
return request
|
|
895
|
-
narrowed_total = max(remaining, 0.0)
|
|
896
|
-
if request.timeout.total is not None:
|
|
897
|
-
narrowed_total = min(request.timeout.total, narrowed_total)
|
|
898
|
-
return replace(
|
|
899
|
-
request,
|
|
900
|
-
timeout=replace(request.timeout, total=narrowed_total),
|
|
901
|
-
)
|