_response_metadata.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. from __future__ import annotations
  2. import contextlib
  3. from contextvars import ContextVar, Token
  4. from typing import TYPE_CHECKING
  5. from .request import Headers
  6. if TYPE_CHECKING:
  7. from collections.abc import Mapping, Sequence
  8. from types import TracebackType
  9. from pyqwest import Headers as HTTPHeaders
  10. _current_response = ContextVar["ResponseMetadata"]("connectrpc_current_response")
  11. def handle_response_headers(headers: HTTPHeaders) -> None:
  12. response = _current_response.get(None)
  13. if not response:
  14. return
  15. response_headers: Headers = Headers()
  16. response_trailers: Headers = Headers()
  17. for key, value in headers.items():
  18. if key.startswith("trailer-"):
  19. normalized_key = key[len("trailer-") :]
  20. obj = response_trailers
  21. else:
  22. normalized_key = key
  23. obj = response_headers
  24. obj.add(normalized_key, value)
  25. if response_headers:
  26. response._headers = response_headers # noqa: SLF001
  27. if response_trailers:
  28. response._trailers = response_trailers # noqa: SLF001
  29. def handle_response_trailers(
  30. trailers: HTTPHeaders | Mapping[str, Sequence[str]],
  31. ) -> None:
  32. response = _current_response.get(None)
  33. if not response:
  34. return
  35. response_trailers = response.trailers
  36. for key, value in trailers.items():
  37. if isinstance(value, str):
  38. response_trailers.add(key, value)
  39. else:
  40. for v in value:
  41. response_trailers.add(key, v)
  42. if response_trailers:
  43. response._trailers = response_trailers # noqa: SLF001
  44. class ResponseMetadata:
  45. """
  46. Response metadata separate from the message payload.
  47. Commonly, RPC client invocations only need the message payload and do not need to
  48. directly read other data such as headers or trailers. In cases where they are needed,
  49. initialize this class in a context manager to access the response headers and trailers
  50. for the invocation made within the context.
  51. Example:
  52. ```python
  53. with ResponseMetadata() as resp_data:
  54. resp = client.MakeHat(Size(inches=10))
  55. do_something_with_response_payload(resp)
  56. check_response_headers(resp_data.headers())
  57. check_response_trailers(resp_data.trailers())
  58. ```
  59. """
  60. _headers: Headers | None = None
  61. _trailers: Headers | None = None
  62. _token: Token[ResponseMetadata] | None = None
  63. def __enter__(self) -> ResponseMetadata:
  64. self._token = _current_response.set(self)
  65. return self
  66. def __exit__(
  67. self,
  68. _exc_type: type[BaseException] | None,
  69. _exc_value: BaseException | None,
  70. _traceback: TracebackType | None,
  71. ) -> None:
  72. if self._token:
  73. # Normal usage with context manager will always work but it is
  74. # theoretically possible for user to move to another thread
  75. # and this fails, it is fine to ignore it.
  76. with contextlib.suppress(Exception):
  77. _current_response.reset(self._token)
  78. self._token = None
  79. @property
  80. def headers(self) -> Headers:
  81. """Returns the response headers."""
  82. if self._headers is None:
  83. return Headers()
  84. return self._headers
  85. @property
  86. def trailers(self) -> Headers:
  87. """Returns the response trailers."""
  88. if self._trailers is None:
  89. return Headers()
  90. return self._trailers