_transport.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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 chattolib's own hand-rolled Connect transport (``chattolib._connect``),
  6. so the library has no third-party Connect dependency.
  7. This module exposes:
  8. * :func:`build_service_clients` — one call, one ``base_url`` argument,
  9. returns a ``ServiceClients`` object with a typed field per Chatto service.
  10. * :func:`translate_connect_error` — translates a
  11. ``chattolib._connect.ConnectError`` into the library's public exception
  12. hierarchy (:class:`chattolib.exceptions.ChattoAuthError` /
  13. :class:`chattolib.exceptions.ChattoConnectError`).
  14. * :func:`pb_to_dict` — turns a protobuf response into the camelCase JSON
  15. shape that the existing ``types.py`` dataclass parsers already accept.
  16. Keeping the parsers dict-driven means the migration from Connect-JSON to
  17. Connect-binary transport doesn't ripple through the entire public API.
  18. """
  19. from __future__ import annotations
  20. from dataclasses import dataclass
  21. from typing import Any
  22. from google.protobuf.json_format import MessageToDict
  23. from google.protobuf.message import Message
  24. from chattolib._connect import Code, ConnectError, google_protobuf_binary_codec
  25. from chattolib._pb.chatto.admin.v1.diagnostics_connect import (
  26. AdminDiagnosticsServiceClient,
  27. )
  28. from chattolib._pb.chatto.admin.v1.event_log_connect import AdminEventLogServiceClient
  29. from chattolib._pb.chatto.admin.v1.invitations_connect import AdminInviteLinkServiceClient
  30. from chattolib._pb.chatto.admin.v1.members_connect import AdminUserServiceClient
  31. from chattolib._pb.chatto.admin.v1.oauth_clients_connect import (
  32. AdminOAuthClientServiceClient,
  33. )
  34. from chattolib._pb.chatto.admin.v1.permissions_connect import (
  35. AdminPermissionServiceClient,
  36. )
  37. from chattolib._pb.chatto.admin.v1.roles_connect import AdminRoleServiceClient
  38. from chattolib._pb.chatto.admin.v1.room_layout_connect import (
  39. AdminRoomLayoutServiceClient,
  40. )
  41. from chattolib._pb.chatto.admin.v1.server_connect import AdminServerServiceClient
  42. from chattolib._pb.chatto.api.v1.account_connect import MyAccountServiceClient
  43. from chattolib._pb.chatto.api.v1.asset_uploads_connect import AssetUploadServiceClient
  44. from chattolib._pb.chatto.api.v1.attachments_connect import AssetServiceClient
  45. from chattolib._pb.chatto.api.v1.bots_connect import BotServiceClient
  46. from chattolib._pb.chatto.api.v1.message_search_connect import (
  47. MessageSearchServiceClient,
  48. )
  49. from chattolib._pb.chatto.api.v1.messages_connect import MessageServiceClient
  50. from chattolib._pb.chatto.api.v1.notifications_connect import (
  51. NotificationPolicyServiceClient,
  52. NotificationServiceClient,
  53. )
  54. from chattolib._pb.chatto.api.v1.permissions_connect import PermissionServiceClient
  55. from chattolib._pb.chatto.api.v1.push_notifications_connect import (
  56. PushNotificationServiceClient,
  57. )
  58. from chattolib._pb.chatto.api.v1.roles_connect import RoleServiceClient
  59. from chattolib._pb.chatto.api.v1.room_directory_connect import (
  60. RoomDirectoryServiceClient,
  61. )
  62. from chattolib._pb.chatto.api.v1.room_invitations_connect import (
  63. RoomInvitationServiceClient,
  64. )
  65. from chattolib._pb.chatto.api.v1.rooms_connect import RoomServiceClient
  66. from chattolib._pb.chatto.api.v1.server_state_connect import ServerServiceClient
  67. from chattolib._pb.chatto.api.v1.threads_connect import ThreadServiceClient
  68. from chattolib._pb.chatto.api.v1.user_service_connect import UserServiceClient
  69. from chattolib._pb.chatto.api.v1.viewer_connect import ViewerServiceClient
  70. from chattolib._pb.chatto.api.v1.voice_calls_connect import VoiceCallServiceClient
  71. from chattolib._pb.chatto.auth.v1.external_identity_auth_connect import (
  72. ExternalIdentityAuthServiceClient,
  73. )
  74. from chattolib._pb.chatto.auth.v1.push_subscription_cleanup_connect import (
  75. PushSubscriptionCleanupServiceClient,
  76. )
  77. from chattolib._pb.chatto.auth.v1.setup_connect import ServerSetupServiceClient
  78. from chattolib._pb.chatto.discovery.v1.server_connect import (
  79. ServerDiscoveryServiceClient,
  80. )
  81. from chattolib.exceptions import ChattoAuthError, ChattoConnectError
  82. CONNECT_PREFIX = "/api/connect"
  83. @dataclass
  84. class ServiceClients:
  85. """Typed bundle of ConnectRPC service clients used by ``ChattoClient``."""
  86. server_discovery: ServerDiscoveryServiceClient
  87. server: ServerServiceClient
  88. viewer: ViewerServiceClient
  89. account: MyAccountServiceClient
  90. roles: RoleServiceClient
  91. room_directory: RoomDirectoryServiceClient
  92. room_invitations: RoomInvitationServiceClient
  93. rooms: RoomServiceClient
  94. messages: MessageServiceClient
  95. threads: ThreadServiceClient
  96. user: UserServiceClient
  97. notifications: NotificationServiceClient
  98. notification_policy: NotificationPolicyServiceClient
  99. message_search: MessageSearchServiceClient
  100. bots: BotServiceClient
  101. permissions: PermissionServiceClient
  102. push: PushNotificationServiceClient
  103. assets: AssetServiceClient
  104. asset_uploads: AssetUploadServiceClient
  105. voice_calls: VoiceCallServiceClient
  106. external_identity_auth: ExternalIdentityAuthServiceClient
  107. push_cleanup: PushSubscriptionCleanupServiceClient
  108. setup: ServerSetupServiceClient
  109. admin_server: AdminServerServiceClient
  110. admin_room_layout: AdminRoomLayoutServiceClient
  111. admin_users: AdminUserServiceClient
  112. admin_roles: AdminRoleServiceClient
  113. admin_event_log: AdminEventLogServiceClient
  114. admin_diagnostics: AdminDiagnosticsServiceClient
  115. admin_permissions: AdminPermissionServiceClient
  116. admin_invite_links: AdminInviteLinkServiceClient
  117. admin_oauth_clients: AdminOAuthClientServiceClient
  118. async def close(self) -> None:
  119. for name in self.__dataclass_fields__:
  120. client = getattr(self, name)
  121. await client.close()
  122. def build_service_clients(base_url: str) -> ServiceClients:
  123. """Instantiate one service client per Chatto Connect service.
  124. ``base_url`` is the server root (e.g. ``https://chat.chatto.run``); the
  125. ConnectRPC prefix is appended by this function.
  126. """
  127. address = f"{base_url.rstrip('/')}{CONNECT_PREFIX}"
  128. codec = google_protobuf_binary_codec()
  129. def make(cls: Any) -> Any:
  130. return cls(address, codec=codec)
  131. return ServiceClients(
  132. server_discovery=make(ServerDiscoveryServiceClient),
  133. server=make(ServerServiceClient),
  134. viewer=make(ViewerServiceClient),
  135. account=make(MyAccountServiceClient),
  136. roles=make(RoleServiceClient),
  137. room_directory=make(RoomDirectoryServiceClient),
  138. room_invitations=make(RoomInvitationServiceClient),
  139. rooms=make(RoomServiceClient),
  140. messages=make(MessageServiceClient),
  141. threads=make(ThreadServiceClient),
  142. user=make(UserServiceClient),
  143. notifications=make(NotificationServiceClient),
  144. notification_policy=make(NotificationPolicyServiceClient),
  145. message_search=make(MessageSearchServiceClient),
  146. bots=make(BotServiceClient),
  147. permissions=make(PermissionServiceClient),
  148. push=make(PushNotificationServiceClient),
  149. assets=make(AssetServiceClient),
  150. asset_uploads=make(AssetUploadServiceClient),
  151. voice_calls=make(VoiceCallServiceClient),
  152. external_identity_auth=make(ExternalIdentityAuthServiceClient),
  153. push_cleanup=make(PushSubscriptionCleanupServiceClient),
  154. setup=make(ServerSetupServiceClient),
  155. admin_server=make(AdminServerServiceClient),
  156. admin_room_layout=make(AdminRoomLayoutServiceClient),
  157. admin_users=make(AdminUserServiceClient),
  158. admin_roles=make(AdminRoleServiceClient),
  159. admin_event_log=make(AdminEventLogServiceClient),
  160. admin_diagnostics=make(AdminDiagnosticsServiceClient),
  161. admin_permissions=make(AdminPermissionServiceClient),
  162. admin_invite_links=make(AdminInviteLinkServiceClient),
  163. admin_oauth_clients=make(AdminOAuthClientServiceClient),
  164. )
  165. def translate_connect_error(exc: ConnectError) -> Exception:
  166. """Convert a Connect transport error into chattolib's exception hierarchy."""
  167. if exc.code == Code.UNAUTHENTICATED:
  168. return ChattoAuthError(str(exc))
  169. return ChattoConnectError(
  170. code=exc.code.name.lower(),
  171. message=str(exc),
  172. )
  173. def pb_to_dict(message: Message | None) -> dict[str, Any]:
  174. """Convert a protobuf message to the camelCase dict shape the parsers accept.
  175. ``preserving_proto_field_name=False`` gives us JSON-mapping camelCase
  176. keys (e.g. ``created_at`` → ``createdAt``), matching what the
  177. ``types.py`` dataclass parsers already consume.
  178. """
  179. if message is None:
  180. return {}
  181. return MessageToDict(
  182. message,
  183. preserving_proto_field_name=False,
  184. use_integers_for_enums=False,
  185. )