_transport.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. # mypy: disable-error-code="no-any-return"
  2. """Shared plumbing for the ConnectRPC-based transport.
  3. Chatto's public API is a ConnectRPC service surface. ``chattolib.client``
  4. speaks to it through generated service stubs (see ``chattolib._pb``) driven
  5. by the official ``connectrpc`` Python package.
  6. This module exposes:
  7. * :func:`build_service_clients` — one call, one ``base_url`` argument,
  8. returns a ``ServiceClients`` object with a typed field per Chatto service.
  9. * :func:`translate_connect_error` — translates a
  10. ``connectrpc.errors.ConnectError`` into the library's public exception
  11. hierarchy (:class:`chattolib.exceptions.ChattoAuthError` /
  12. :class:`chattolib.exceptions.ChattoConnectError`).
  13. * :func:`pb_to_dict` — turns a protobuf response into the camelCase JSON
  14. shape that the existing ``types.py`` dataclass parsers already accept.
  15. Keeping the parsers dict-driven means the migration from Connect-JSON to
  16. Connect-binary transport doesn't ripple through the entire public API.
  17. """
  18. from __future__ import annotations
  19. from dataclasses import dataclass
  20. from typing import Any
  21. from connectrpc.code import Code
  22. from connectrpc.compat import google_protobuf_binary_codec
  23. from connectrpc.errors import ConnectError
  24. from google.protobuf.json_format import MessageToDict
  25. from google.protobuf.message import Message
  26. from chattolib._pb.chatto.admin.v1.diagnostics_connect import (
  27. AdminDiagnosticsServiceClient,
  28. )
  29. from chattolib._pb.chatto.admin.v1.event_log_connect import AdminEventLogServiceClient
  30. from chattolib._pb.chatto.admin.v1.members_connect import AdminUserServiceClient
  31. from chattolib._pb.chatto.admin.v1.permissions_connect import (
  32. AdminPermissionServiceClient,
  33. )
  34. from chattolib._pb.chatto.admin.v1.roles_connect import AdminRoleServiceClient
  35. from chattolib._pb.chatto.admin.v1.room_layout_connect import (
  36. AdminRoomLayoutServiceClient,
  37. )
  38. from chattolib._pb.chatto.admin.v1.server_connect import AdminServerServiceClient
  39. from chattolib._pb.chatto.api.v1.account_connect import MyAccountServiceClient
  40. from chattolib._pb.chatto.api.v1.asset_uploads_connect import AssetUploadServiceClient
  41. from chattolib._pb.chatto.api.v1.attachments_connect import AssetServiceClient
  42. from chattolib._pb.chatto.api.v1.member_directory_connect import UserServiceClient
  43. from chattolib._pb.chatto.api.v1.messages_connect import MessageServiceClient
  44. from chattolib._pb.chatto.api.v1.notification_preferences_connect import (
  45. NotificationPreferencesServiceClient,
  46. )
  47. from chattolib._pb.chatto.api.v1.notifications_connect import (
  48. NotificationServiceClient,
  49. )
  50. from chattolib._pb.chatto.api.v1.push_notifications_connect import (
  51. PushNotificationServiceClient,
  52. )
  53. from chattolib._pb.chatto.api.v1.roles_connect import RoleServiceClient
  54. from chattolib._pb.chatto.api.v1.room_directory_connect import (
  55. RoomDirectoryServiceClient,
  56. )
  57. from chattolib._pb.chatto.api.v1.rooms_connect import RoomServiceClient
  58. from chattolib._pb.chatto.api.v1.server_state_connect import ServerServiceClient
  59. from chattolib._pb.chatto.api.v1.threads_connect import ThreadServiceClient
  60. from chattolib._pb.chatto.api.v1.viewer_connect import ViewerServiceClient
  61. from chattolib._pb.chatto.api.v1.voice_calls_connect import VoiceCallServiceClient
  62. from chattolib._pb.chatto.auth.v1.external_identity_auth_connect import (
  63. ExternalIdentityAuthServiceClient,
  64. )
  65. from chattolib._pb.chatto.discovery.v1.server_connect import (
  66. ServerDiscoveryServiceClient,
  67. )
  68. from chattolib.exceptions import ChattoAuthError, ChattoConnectError
  69. CONNECT_PREFIX = "/api/connect"
  70. @dataclass
  71. class ServiceClients:
  72. """Typed bundle of ConnectRPC service clients used by ``ChattoClient``."""
  73. server_discovery: ServerDiscoveryServiceClient
  74. server: ServerServiceClient
  75. viewer: ViewerServiceClient
  76. account: MyAccountServiceClient
  77. users: UserServiceClient
  78. roles: RoleServiceClient
  79. room_directory: RoomDirectoryServiceClient
  80. rooms: RoomServiceClient
  81. messages: MessageServiceClient
  82. threads: ThreadServiceClient
  83. notifications: NotificationServiceClient
  84. notification_prefs: NotificationPreferencesServiceClient
  85. push: PushNotificationServiceClient
  86. assets: AssetServiceClient
  87. asset_uploads: AssetUploadServiceClient
  88. voice_calls: VoiceCallServiceClient
  89. external_auth: ExternalIdentityAuthServiceClient
  90. admin_server: AdminServerServiceClient
  91. admin_room_layout: AdminRoomLayoutServiceClient
  92. admin_users: AdminUserServiceClient
  93. admin_roles: AdminRoleServiceClient
  94. admin_event_log: AdminEventLogServiceClient
  95. admin_diagnostics: AdminDiagnosticsServiceClient
  96. admin_permissions: AdminPermissionServiceClient
  97. async def close(self) -> None:
  98. for name in self.__dataclass_fields__:
  99. client = getattr(self, name)
  100. await client.close()
  101. def build_service_clients(base_url: str) -> ServiceClients:
  102. """Instantiate one service client per Chatto Connect service.
  103. ``base_url`` is the server root (e.g. ``https://chat.chatto.run``); the
  104. ConnectRPC prefix is appended by this function.
  105. """
  106. address = f"{base_url.rstrip('/')}{CONNECT_PREFIX}"
  107. codec = google_protobuf_binary_codec()
  108. def make(cls: Any) -> Any:
  109. return cls(address, codec=codec)
  110. return ServiceClients(
  111. server_discovery=make(ServerDiscoveryServiceClient),
  112. server=make(ServerServiceClient),
  113. viewer=make(ViewerServiceClient),
  114. account=make(MyAccountServiceClient),
  115. users=make(UserServiceClient),
  116. roles=make(RoleServiceClient),
  117. room_directory=make(RoomDirectoryServiceClient),
  118. rooms=make(RoomServiceClient),
  119. messages=make(MessageServiceClient),
  120. threads=make(ThreadServiceClient),
  121. notifications=make(NotificationServiceClient),
  122. notification_prefs=make(NotificationPreferencesServiceClient),
  123. push=make(PushNotificationServiceClient),
  124. assets=make(AssetServiceClient),
  125. asset_uploads=make(AssetUploadServiceClient),
  126. voice_calls=make(VoiceCallServiceClient),
  127. external_auth=make(ExternalIdentityAuthServiceClient),
  128. admin_server=make(AdminServerServiceClient),
  129. admin_room_layout=make(AdminRoomLayoutServiceClient),
  130. admin_users=make(AdminUserServiceClient),
  131. admin_roles=make(AdminRoleServiceClient),
  132. admin_event_log=make(AdminEventLogServiceClient),
  133. admin_diagnostics=make(AdminDiagnosticsServiceClient),
  134. admin_permissions=make(AdminPermissionServiceClient),
  135. )
  136. def translate_connect_error(exc: ConnectError) -> Exception:
  137. """Convert a ``connectrpc`` error into chattolib's exception hierarchy."""
  138. if exc.code == Code.UNAUTHENTICATED:
  139. return ChattoAuthError(str(exc))
  140. return ChattoConnectError(
  141. code=exc.code.name.lower(),
  142. message=str(exc),
  143. )
  144. def pb_to_dict(message: Message | None) -> dict[str, Any]:
  145. """Convert a protobuf message to the camelCase dict shape the parsers accept.
  146. ``preserving_proto_field_name=False`` gives us JSON-mapping camelCase
  147. keys (e.g. ``created_at`` → ``createdAt``), matching what the
  148. ``types.py`` dataclass parsers already consume.
  149. """
  150. if message is None:
  151. return {}
  152. return MessageToDict(
  153. message,
  154. preserving_proto_field_name=False,
  155. use_integers_for_enums=False,
  156. )