Explorar el Código

refactor: drop the connectrpc dependency from the runtime

Complete the removal of the connectrpc (and transitively pyqwest)
dependency. The T1-T4 work added chattolib._connect and the self-contained
protoc plugin, but _transport.py and client.py still imported Code,
ConnectError and the binary codec from connectrpc — so the package would not
have imported at all once connectrpc was uninstalled.

- Rewire _transport.py and client.py to import Code / ConnectError /
  google_protobuf_binary_codec from chattolib._connect.
- Declare httpx as a hard dependency (the hand-rolled transport uses it
  unconditionally; it was previously pulled in transitively via connectrpc).
  websockets stays in the [realtime] extra.
- Update the module docstrings that still described the transport as going
  through the connectrpc package.
- Exclude the generated chattolib/_codegen descriptor stubs from ruff/mypy,
  matching the existing _pb exclusion.

Verified: with connectrpc import-blocked at the interpreter level,
'import chattolib' and Bot both work; the full suite is green (70 passed)
and a live GetServer RPC against the preview server round-trips real data.
Federico Feliziani hace 2 meses
padre
commit
db860e9e06
Se han modificado 4 ficheros con 25 adiciones y 24 borrados
  1. 4 3
      pyproject.toml
  2. 9 7
      src/chattolib/_protoc_plugin.py
  3. 5 6
      src/chattolib/_transport.py
  4. 7 8
      src/chattolib/client.py

+ 4 - 3
pyproject.toml

@@ -26,6 +26,7 @@ classifiers = [
 ]
 dependencies = [
     "protobuf>=5.28",
+    "httpx>=0.27",
 ]
 
 [project.urls]
@@ -53,7 +54,7 @@ testpaths = ["tests"]
 [tool.ruff]
 target-version = "py311"
 line-length = 100
-extend-exclude = ["src/chattolib/_pb"]
+extend-exclude = ["src/chattolib/_pb", "src/chattolib/_codegen"]
 
 [tool.ruff.lint]
 select = ["E", "F", "I", "N", "W", "UP"]
@@ -63,9 +64,9 @@ python_version = "3.11"
 strict = true
 warn_return_any = true
 warn_unused_configs = true
-exclude = ["src/chattolib/_pb"]
+exclude = ["src/chattolib/_pb", "src/chattolib/_codegen"]
 
 [[tool.mypy.overrides]]
-module = ["chattolib._pb.*", "websockets.*"]
+module = ["chattolib._pb.*", "chattolib._codegen.*", "websockets.*"]
 follow_imports = "skip"
 ignore_missing_imports = true

+ 9 - 7
src/chattolib/_protoc_plugin.py

@@ -21,32 +21,34 @@ from typing import Any
 _CODEGEN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_codegen")
 
 
-def _load_codegen_module(name: str):
+def _load_codegen_module(name: str) -> Any:
     """Load a vendored codegen descriptor module directly from its file path.
 
     The descriptors live under ``chattolib._codegen``, but importing them through
     the ``chattolib`` package would execute ``chattolib/__init__.py`` (which
     eagerly imports the client) — undesirable for a standalone ``protoc`` plugin
-    process. Loading by file path avoids that. The one cross-import between the
-    two descriptor modules is resolved by pre-registering the request module in
-    ``sys.modules`` under the name the response module expects.
+    process. Loading by file path avoids that.
     """
     path = os.path.join(_CODEGEN_DIR, f"{name}.py")
     spec = importlib.util.spec_from_file_location(f"_chattolib_codegen_{name}", path)
+    assert spec is not None
     mod = importlib.util.module_from_spec(spec)
     sys.modules[spec.name] = mod
-    spec.loader.exec_module(mod)  # type: ignore[union-attr]
+    assert spec.loader is not None
+    spec.loader.exec_module(mod)
     return mod
 
 
-def _load_descriptors():
+def _load_descriptors() -> tuple[Any, Any]:
     return (
         _load_codegen_module("protoc_gen_request_pb2"),
         _load_codegen_module("protoc_gen_response_pb2"),
     )
 
 
-g, r = _load_descriptors()
+_g, _r = _load_descriptors()
+g: Any = _g
+r: Any = _r
 
 _HEADER = "# Generated by chattolib's protoc plugin (protoc-gen-chattolib). DO NOT EDIT!\n"
 

+ 5 - 6
src/chattolib/_transport.py

@@ -3,14 +3,15 @@
 
 Chatto's public API is a ConnectRPC service surface. ``chattolib.client``
 speaks to it through generated service stubs (see ``chattolib._pb``) driven
-by the official ``connectrpc`` Python package.
+by chattolib's own hand-rolled Connect transport (``chattolib._connect``),
+so the library has no third-party Connect dependency.
 
 This module exposes:
 
 * :func:`build_service_clients` — one call, one ``base_url`` argument,
   returns a ``ServiceClients`` object with a typed field per Chatto service.
 * :func:`translate_connect_error` — translates a
-  ``connectrpc.errors.ConnectError`` into the library's public exception
+  ``chattolib._connect.ConnectError`` into the library's public exception
   hierarchy (:class:`chattolib.exceptions.ChattoAuthError` /
   :class:`chattolib.exceptions.ChattoConnectError`).
 * :func:`pb_to_dict` — turns a protobuf response into the camelCase JSON
@@ -25,12 +26,10 @@ from __future__ import annotations
 from dataclasses import dataclass
 from typing import Any
 
-from connectrpc.code import Code
-from connectrpc.compat import google_protobuf_binary_codec
-from connectrpc.errors import ConnectError
 from google.protobuf.json_format import MessageToDict
 from google.protobuf.message import Message
 
+from chattolib._connect import Code, ConnectError, google_protobuf_binary_codec
 from chattolib._pb.chatto.admin.v1.diagnostics_connect import (
     AdminDiagnosticsServiceClient,
 )
@@ -149,7 +148,7 @@ def build_service_clients(base_url: str) -> ServiceClients:
 
 
 def translate_connect_error(exc: ConnectError) -> Exception:
-    """Convert a ``connectrpc`` error into chattolib's exception hierarchy."""
+    """Convert a Connect transport error into chattolib's exception hierarchy."""
     if exc.code == Code.UNAUTHENTICATED:
         return ChattoAuthError(str(exc))
     return ChattoConnectError(

+ 7 - 8
src/chattolib/client.py

@@ -1,10 +1,10 @@
 """Main async client for the Chatto Connect API.
 
 Chatto migrated from GraphQL to a protobuf-first Connect API in v0.4.x
-(see ADR-042). The client speaks Connect binary protobuf via the official
-``connectrpc`` Python package and the generated service stubs under
-``chattolib._pb`` for all request/response operations. Realtime events
-live in ``chattolib.realtime``.
+(see ADR-042). The client speaks Connect binary protobuf over a
+hand-rolled transport (``chattolib._connect``) and the generated service
+stubs under ``chattolib._pb`` for all request/response operations. Realtime
+events live in ``chattolib.realtime``.
 """
 
 # mypy: disable-error-code="no-any-return"
@@ -21,10 +21,9 @@ from datetime import datetime
 from pathlib import Path
 from typing import Any, TypeVar
 
-# ConnectError isn't re-exported publicly by connectrpc.__init__; import from
-# its submodule so the top-level import path stays clean for callers.
-from connectrpc.errors import ConnectError  # noqa: E402
-
+# ConnectError is raised by our hand-rolled Connect client (chattolib._connect);
+# catch it here to translate into the library's public exception hierarchy.
+from chattolib._connect import ConnectError  # noqa: E402
 from chattolib._pb.chatto.admin.v1 import (
     event_log_pb2,
     room_layout_pb2,