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.
Files changed (56) hide show
  1. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/METADATA +21 -24
  2. sg_python_api-0.2.0.dist-info/RECORD +58 -0
  3. shotgun_api3/__init__.py +4 -0
  4. shotgun_api3/_async_client.py +347 -281
  5. shotgun_api3/_async_driver.py +25 -55
  6. shotgun_api3/_async_httpx2.py +92 -59
  7. shotgun_api3/_async_transport.py +38 -3
  8. shotgun_api3/_capabilities.py +4 -3
  9. shotgun_api3/_client.py +296 -422
  10. shotgun_api3/_config.py +245 -120
  11. shotgun_api3/_connections.py +144 -0
  12. shotgun_api3/_crud.py +25 -250
  13. shotgun_api3/_driver.py +29 -54
  14. shotgun_api3/_execution.py +1 -3
  15. shotgun_api3/_files.py +193 -21
  16. shotgun_api3/_methods.py +59 -411
  17. shotgun_api3/_protocol.py +27 -53
  18. shotgun_api3/_transport.py +82 -407
  19. shotgun_api3/_version.py +7 -2
  20. shotgun_api3/lib/mockgun/__init__.py +20 -7
  21. shotgun_api3/lib/mockgun/_activity.py +510 -0
  22. shotgun_api3/lib/mockgun/_async_client.py +196 -0
  23. shotgun_api3/lib/mockgun/_dates.py +164 -0
  24. shotgun_api3/lib/mockgun/_errors.py +18 -0
  25. shotgun_api3/lib/mockgun/_events.py +1031 -0
  26. shotgun_api3/lib/mockgun/_follows.py +217 -0
  27. shotgun_api3/lib/mockgun/_handlers.py +558 -0
  28. shotgun_api3/lib/mockgun/_links.py +440 -0
  29. shotgun_api3/lib/mockgun/_new_client.py +92 -0
  30. shotgun_api3/lib/mockgun/_presets.py +492 -0
  31. shotgun_api3/lib/mockgun/_query.py +2163 -0
  32. shotgun_api3/lib/mockgun/_retirement.py +368 -0
  33. shotgun_api3/lib/mockgun/_schema.py +172 -0
  34. shotgun_api3/lib/mockgun/_schema_writes.py +425 -0
  35. shotgun_api3/lib/mockgun/_search.py +378 -0
  36. shotgun_api3/lib/mockgun/_server.py +169 -0
  37. shotgun_api3/lib/mockgun/_sessions.py +206 -0
  38. shotgun_api3/lib/mockgun/_site.py +150 -0
  39. shotgun_api3/lib/mockgun/_site_data.py +584 -0
  40. shotgun_api3/lib/mockgun/_storage.py +208 -0
  41. shotgun_api3/lib/mockgun/_store.py +1446 -0
  42. shotgun_api3/lib/mockgun/_summaries.py +964 -0
  43. shotgun_api3/lib/mockgun/_sync_client.py +190 -0
  44. shotgun_api3/lib/mockgun/_threads.py +226 -0
  45. shotgun_api3/lib/mockgun/_thumbnails.py +146 -0
  46. shotgun_api3/lib/mockgun/_transport.py +145 -0
  47. shotgun_api3/lib/mockgun/_uploads.py +417 -0
  48. shotgun_api3/lib/mockgun/_wire.py +120 -0
  49. shotgun_api3/lib/mockgun/mockgun.py +14 -0
  50. shotgun_api3/lib/sgtimezone.py +24 -1
  51. sg_python_api-0.1.2.dist-info/RECORD +0 -31
  52. shotgun_api3/_signatures.py +0 -53
  53. shotgun_api3/lib/mockgun/_async.py +0 -315
  54. shotgun_api3/lib/mockgun/_mockgun.py +0 -1666
  55. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/WHEEL +0 -0
  56. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 contextlib import suppress
16
- from dataclasses import dataclass, field, replace
9
+ from dataclasses import dataclass, field
17
10
  from types import MappingProxyType
18
- from typing import TYPE_CHECKING, Any, Protocol, cast
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
- """Release resources associated with the streaming response."""
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/read limits and total monotonic ceiling through body consumption.
171
+ """Connect and read timeout limits.
174
172
 
175
- The total limit applies per attempt. A `None` field has no timeout limit.
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
- """Release resources associated with the buffered response."""
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
- pool_classes: dict[
512
- str,
513
- type[
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
- ) -> _StartedUrllib3Request:
529
- """Start one urllib3 request and leave its live handle caller-owned."""
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
- try:
539
- started = time.monotonic()
540
- response = cast(
541
- 'urllib3.response.HTTPResponse',
542
- self._pool.request(
543
- request.method,
544
- request.url,
545
- body=request.body,
546
- headers=request.headers,
547
- timeout=timeout,
548
- retries=False,
549
- preload_content=preload_content,
550
- ),
551
- )
552
- except urllib3.exceptions.HTTPError as exc:
553
- if self._abort_registry._was_aborted(handle):
554
- raise AbortedError(request.token) from exc
555
- raise _map_urllib3_error(exc) from exc
556
- except urllib3.exceptions.ProxySchemeUnsupported as exc:
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
- started = self._start_request(request, preload_content=True)
607
- try:
608
- return TransportResponse(
609
- status=started.response.status,
610
- reason=started.response.reason or '',
611
- headers=dict(started.response.headers),
612
- content=started.response.data,
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
- started = self._start_request(request, preload_content=False)
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, self._abort_registry, started.handle)
458
+ _release_urllib3_stream(response)
639
459
 
640
460
  def stream(size: int) -> Iterator[bytes]:
641
- attempt = _Urllib3StreamingAttempt(
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
- if attempt.registry._was_aborted(attempt.handle):
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
- """Release one streaming registry handle and any owned connection."""
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
- attempt: _Urllib3StreamingAttempt,
532
+ response: urllib3.response.HTTPResponse,
825
533
  size: int,
826
534
  ) -> Iterator[bytes]:
827
- """Yield body chunks while mapping errors and enforcing the deadline."""
828
- chunks = attempt.response.stream(size)
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
- try:
836
- chunk = _next_urllib3_stream_chunk(attempt, chunks, total_limited)
837
- except StopIteration:
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
- )