_glue.py 3.6 KB

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