_glue.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. from __future__ import annotations
  2. import contextlib
  3. import inspect
  4. import types
  5. from typing import TYPE_CHECKING, Protocol, TypeVar
  6. from ._multipart import (
  7. encode_multipart,
  8. encode_multipart_sync,
  9. multipart_boundary,
  10. multipart_content_type,
  11. )
  12. from ._pyqwest import FullResponse, Headers, Request, Transport
  13. if TYPE_CHECKING:
  14. from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
  15. from ._multipart import Multipart, SyncMultipart
  16. T_contra = TypeVar("T_contra", contravariant=True)
  17. U = TypeVar("U")
  18. async def wrap_body_gen(
  19. gen: AsyncIterator[T_contra],
  20. wrap_fn: Callable[[T_contra], U],
  21. start: Awaitable[bool],
  22. ) -> AsyncIterator[U]:
  23. try:
  24. if not await start:
  25. return
  26. async for item in gen:
  27. yield wrap_fn(item)
  28. finally:
  29. try:
  30. aclose = gen.aclose # ty: ignore[unresolved-attribute]
  31. except AttributeError:
  32. pass
  33. else:
  34. await aclose()
  35. async def new_full_response(
  36. status: int,
  37. headers: Headers,
  38. content: AsyncIterator[memoryview | bytes | bytearray],
  39. trailers: Headers,
  40. ) -> FullResponse:
  41. buf = bytearray()
  42. try:
  43. async for chunk in content:
  44. buf.extend(chunk)
  45. finally:
  46. try:
  47. aclose = content.aclose # ty: ignore[unresolved-attribute]
  48. except AttributeError:
  49. pass
  50. else:
  51. await aclose()
  52. return FullResponse(status, headers, bytes(buf), trailers)
  53. async def execute_and_read_full(transport: Transport, request: Request) -> FullResponse:
  54. resp = await transport.execute(request)
  55. return await new_full_response(
  56. resp.status, resp.headers, resp.content, resp.trailers
  57. )
  58. def read_content_sync(content: Iterator[bytes | memoryview]) -> bytes:
  59. buf = bytearray()
  60. try:
  61. for chunk in content:
  62. buf.extend(chunk)
  63. finally:
  64. try:
  65. close = content.close # ty: ignore[unresolved-attribute]
  66. except AttributeError:
  67. pass
  68. else:
  69. close()
  70. return bytes(buf)
  71. def multipart_content(multipart: Multipart) -> tuple[str, AsyncIterator[bytes]]:
  72. boundary = multipart_boundary()
  73. return multipart_content_type(boundary), encode_multipart(multipart, boundary)
  74. def multipart_content_sync(multipart: SyncMultipart) -> tuple[str, Iterator[bytes]]:
  75. boundary = multipart_boundary()
  76. return multipart_content_type(boundary), encode_multipart_sync(multipart, boundary)
  77. def close_request_iterator(itr: Iterator[bytes]) -> None:
  78. # Running generators cannot be closed reliably.
  79. # On Python 3.12, it can cause a hang.
  80. if (
  81. isinstance(itr, types.GeneratorType)
  82. and inspect.getgeneratorstate(itr) == inspect.GEN_RUNNING
  83. ):
  84. return
  85. try:
  86. close = itr.close # ty: ignore[unresolved-attribute]
  87. except AttributeError:
  88. pass
  89. else:
  90. with contextlib.suppress(Exception):
  91. close()
  92. # Vendored from pyo3-async-runtimes to apply some fixes
  93. class Sender(Protocol[T_contra]):
  94. def send(self, item: T_contra | BaseException) -> bool | Awaitable[bool]: ...
  95. def close(self) -> None: ...
  96. async def forward(gen: AsyncIterator[T_contra], sender: Sender[T_contra]) -> None:
  97. try:
  98. async for item in gen:
  99. should_continue = sender.send(item)
  100. if inspect.isawaitable(should_continue):
  101. should_continue = await should_continue
  102. if should_continue:
  103. continue
  104. break
  105. except Exception as e:
  106. res = sender.send(e)
  107. if inspect.isawaitable(res):
  108. await res
  109. finally:
  110. sender.close()