errors.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. from __future__ import annotations
  2. __all__ = ["ConnectError", "ErrorDetail"]
  3. from typing import TYPE_CHECKING, TypeVar, overload
  4. from protobuf import Message, Registry
  5. from protobuf.wkt import Any
  6. if TYPE_CHECKING:
  7. from collections.abc import Iterable, Sequence
  8. from protobuf import DescMessage
  9. from .code import Code
  10. T = TypeVar("T", bound=Message)
  11. class ErrorDetail:
  12. """A self-describing Protobuf message attached to a [ConnectError][].
  13. Error details are sent over the network to clients, which can then work with
  14. strongly-typed data rather than trying to parse a complex error message. For
  15. example, you might use details to send a localized error message or retry
  16. parameters to a client.
  17. """
  18. def __init__(self, message: Message) -> None:
  19. if isinstance(message, Any):
  20. self._message = None
  21. self._any = message
  22. return
  23. self._message = message
  24. self._any = Any.pack(message)
  25. @property
  26. def type_name(self) -> str:
  27. """The fully-qualified name of the details Protobuf message (for example, acme.foo.v1.FooDetail)."""
  28. return self._any.type_url.removeprefix("type.googleapis.com/")
  29. @property
  30. def message_bytes(self) -> bytes:
  31. """The Protobuf message serialized as bytes."""
  32. return self._any.value
  33. @overload
  34. def value(self) -> Message | None: ...
  35. @overload
  36. def value(self, desc: DescMessage, /) -> Message | None: ...
  37. @overload
  38. def value(self, typ: type[T], /) -> T | None: ...
  39. @overload
  40. def value(self, registry: Registry, /) -> Message | None: ...
  41. def value(
  42. self, desc_or_registry: Registry | DescMessage | type[Message] | None = None
  43. ) -> Message | None:
  44. """The details message as a Protobuf message, or None if it cannot be deserialized."""
  45. if self._message:
  46. return self._message
  47. if not desc_or_registry:
  48. return None
  49. if isinstance(desc_or_registry, Registry):
  50. desc = desc_or_registry.message(self.type_name)
  51. if not desc:
  52. return None
  53. else:
  54. desc = desc_or_registry
  55. return self._any.unpack(desc)
  56. class ConnectError(Exception):
  57. """An exception in a Connect RPC.
  58. If a server raises a ConnectError, the same exception content will be
  59. raised on the client as well. Errors surfacing on the client side such as
  60. timeouts will also be raised as a ConnectError with an appropriate
  61. [Code][].
  62. """
  63. def __init__(
  64. self, code: Code, message: str, details: Iterable[Message | ErrorDetail] = ()
  65. ) -> None:
  66. """
  67. Creates a new Connect error.
  68. Args:
  69. code: The error code.
  70. message: The error message.
  71. details: Additional details about the error.
  72. """
  73. super().__init__(message)
  74. self._code = code
  75. self._message = message
  76. self._details = (
  77. [m if isinstance(m, ErrorDetail) else ErrorDetail(m) for m in details]
  78. if details
  79. else ()
  80. )
  81. @property
  82. def code(self) -> Code:
  83. return self._code
  84. @property
  85. def message(self) -> str:
  86. return self._message
  87. @property
  88. def details(self) -> Sequence[ErrorDetail]:
  89. return self._details