from __future__ import annotations import contextlib from contextvars import ContextVar, Token from typing import TYPE_CHECKING from .request import Headers if TYPE_CHECKING: from collections.abc import Mapping, Sequence from types import TracebackType from pyqwest import Headers as HTTPHeaders _current_response = ContextVar["ResponseMetadata"]("connectrpc_current_response") def handle_response_headers(headers: HTTPHeaders) -> None: response = _current_response.get(None) if not response: return response_headers: Headers = Headers() response_trailers: Headers = Headers() for key, value in headers.items(): if key.startswith("trailer-"): normalized_key = key[len("trailer-") :] obj = response_trailers else: normalized_key = key obj = response_headers obj.add(normalized_key, value) if response_headers: response._headers = response_headers # noqa: SLF001 if response_trailers: response._trailers = response_trailers # noqa: SLF001 def handle_response_trailers( trailers: HTTPHeaders | Mapping[str, Sequence[str]], ) -> None: response = _current_response.get(None) if not response: return response_trailers = response.trailers for key, value in trailers.items(): if isinstance(value, str): response_trailers.add(key, value) else: for v in value: response_trailers.add(key, v) if response_trailers: response._trailers = response_trailers # noqa: SLF001 class ResponseMetadata: """ Response metadata separate from the message payload. Commonly, RPC client invocations only need the message payload and do not need to directly read other data such as headers or trailers. In cases where they are needed, initialize this class in a context manager to access the response headers and trailers for the invocation made within the context. Example: ```python with ResponseMetadata() as resp_data: resp = client.MakeHat(Size(inches=10)) do_something_with_response_payload(resp) check_response_headers(resp_data.headers()) check_response_trailers(resp_data.trailers()) ``` """ _headers: Headers | None = None _trailers: Headers | None = None _token: Token[ResponseMetadata] | None = None def __enter__(self) -> ResponseMetadata: self._token = _current_response.set(self) return self def __exit__( self, _exc_type: type[BaseException] | None, _exc_value: BaseException | None, _traceback: TracebackType | None, ) -> None: if self._token: # Normal usage with context manager will always work but it is # theoretically possible for user to move to another thread # and this fails, it is fine to ignore it. with contextlib.suppress(Exception): _current_response.reset(self._token) self._token = None @property def headers(self) -> Headers: """Returns the response headers.""" if self._headers is None: return Headers() return self._headers @property def trailers(self) -> Headers: """Returns the response trailers.""" if self._trailers is None: return Headers() return self._trailers