_envelope.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. from __future__ import annotations
  2. import struct
  3. from abc import ABC, abstractmethod
  4. from typing import TYPE_CHECKING, Any, Generic, TypeVar
  5. from ._compression import Compression, IdentityCompression
  6. from .code import Code
  7. from .errors import ConnectError
  8. if TYPE_CHECKING:
  9. from collections.abc import Iterator
  10. from pyqwest import Response, SyncResponse
  11. from ._codec import Codec
  12. from ._protocol import ConnectWireError
  13. from .request import Headers
  14. _RES = TypeVar("_RES")
  15. _T = TypeVar("_T")
  16. class EnvelopeReader(Generic[_RES]):
  17. _next_message_length: int | None
  18. def __init__(
  19. self,
  20. message_class: type[_RES],
  21. codec: Codec,
  22. compression: Compression,
  23. read_max_bytes: int | None,
  24. ) -> None:
  25. self._buffer = bytearray()
  26. self._message_class = message_class
  27. self._codec = codec
  28. self._compression = compression
  29. self._read_max_bytes = read_max_bytes
  30. self._next_message_length = None
  31. def feed(self, data: bytes | memoryview | bytearray) -> Iterator[_RES]:
  32. self._buffer.extend(data)
  33. return self._read_messages()
  34. def _read_messages(self) -> Iterator[_RES]:
  35. while self._buffer:
  36. if self._next_message_length is not None:
  37. if len(self._buffer) < self._next_message_length + 5:
  38. return
  39. prefix_byte = self._buffer[0]
  40. compressed = prefix_byte & 0b01 != 0
  41. message_data = self._buffer[5 : 5 + self._next_message_length]
  42. self._buffer = self._buffer[5 + self._next_message_length :]
  43. self._next_message_length = None
  44. if compressed:
  45. if isinstance(self._compression, IdentityCompression):
  46. raise ConnectError(
  47. Code.INTERNAL,
  48. "protocol error: sent compressed message without compression support",
  49. )
  50. message_data = self._compression.decompress(message_data)
  51. if (
  52. self._read_max_bytes is not None
  53. and len(message_data) > self._read_max_bytes
  54. ):
  55. raise ConnectError(
  56. Code.RESOURCE_EXHAUSTED,
  57. f"message is larger than configured max {self._read_max_bytes}",
  58. )
  59. if self.handle_end_message(prefix_byte, message_data):
  60. return
  61. res = self._codec.decode(message_data, self._message_class)
  62. yield res
  63. if len(self._buffer) < 5:
  64. return
  65. self._next_message_length = int.from_bytes(self._buffer[1:5], "big")
  66. def handle_end_message(
  67. self, prefix_byte: int, message_data: bytes | bytearray
  68. ) -> bool:
  69. """For client protocols with an end message like Connect and gRPC-Web, handle the end message.
  70. Returns True if the end message was handled, False otherwise.
  71. """
  72. return False
  73. def handle_response_complete(
  74. self, response: Response | SyncResponse, e: ConnectError | None = None
  75. ) -> None:
  76. """Handle any client finalization needed when the response is complete.
  77. This is typically used to process trailers for gRPC.
  78. """
  79. class EnvelopeWriter(ABC, Generic[_T]):
  80. def __init__(self, codec: Codec[_T, Any], compression: Compression | None) -> None:
  81. self._codec = codec
  82. self._compression = compression
  83. self._prefix = (
  84. 0 if not compression or isinstance(compression, IdentityCompression) else 1
  85. )
  86. def write(self, message: _T) -> bytes:
  87. data = self._codec.encode(message)
  88. if self._compression:
  89. data = self._compression.compress(data)
  90. # This copies data into the final envelope, but it is still better than issuing
  91. # I/O multiple times for small prefix / length elements.
  92. return struct.pack(">BI", self._prefix, len(data)) + data
  93. @abstractmethod
  94. def end(
  95. self, user_trailers: Headers, error: ConnectWireError | None
  96. ) -> bytes | Headers: ...