Просмотр исходного кода

Import vendored chattolib directly, drop lazy_import and SingletonSlot

With chattolib vendored, the lazy_import indirection for every symbol
and the AsyncSingletonSlot client slot go away — imports resolve
straight from vendor/, typed and checkable. The scattered env-var
config dict handling is collected into a ChattoConfig dataclass in
platform_config.py, and validate_config()/__init__ carry real
PlatformConfig types. _decode_event_envelope() decodes realtime frames
via the vendored pb with a JSON fallback.
Paul Klumpp 1 неделя назад
Родитель
Сommit
e182d8c001
4 измененных файлов с 528 добавлено и 218 удалено
  1. 378 208
      adapter.py
  2. 70 0
      platform_config.py
  3. 42 10
      test_adapter.py
  4. 38 0
      vendor_chattolib.sh

Разница между файлами не показана из-за своего большого размера
+ 378 - 208
adapter.py


+ 70 - 0
platform_config.py

@@ -0,0 +1,70 @@
+"""
+Chatto Platform Config
+
+
+"""
+from dataclasses import dataclass
+import os
+from typing import Optional
+
+import utils
+
+
+
+
+@dataclass
+class ChattoConfig:
+    """
+    Chatto Platform Config
+
+    Some constants and getting from environment/config.yaml
+    """
+
+    _token: Optional[str] = None
+    _PLATFORM: str = "CHATTO"
+
+    def __init__(self, extra: dict = {}):
+
+        self._base_url = os.getenv("CHATTO_BASE_URL", "").strip() # todo: add config.yaml support
+        self._token = os.getenv("CHATTO_TOKEN", "").strip() # todo: add config.yaml support 
+        self._login = os.getenv("CHATTO_LOGIN", "").strip() # todo: add config.yaml support
+        self._password = os.getenv("CHATTO_PASSWORD", "").strip() # todo: add config.yaml support
+
+        self._raw_channels = os.getenv("CHATTO_CHANNELS", "").strip() # todo: add config.yaml support
+
+        if self._raw_channels:
+            self._channel_ids = [c.strip() for c in self._raw_channels.split(",") if c.strip()]
+        elif isinstance(extra.get("channels"), list):
+            self._channel_ids = [str(c) for c in extra["channels"]]
+        else:
+            self._channel_ids = []
+
+
+        self._home_channel = (
+            os.getenv("CHATTO_HOME_CHANNEL", "").strip()
+            or str(extra.get("home_channel", "")).strip()
+        )
+
+        self._require_mention = os.getenv("CHATTO_REQUIRE_MENTION", "").strip().lower()
+        if self._require_mention:
+            self._require_mention = self._require_mention in ("true", "1", "yes")
+        else:
+            self._require_mention = bool(extra.get("require_mention", True))
+
+        # free_response_channels: room IDs where the bot responds without being mentioned via "@botname"
+        _free_response = os.getenv("CHATTO_FREE_RESPONSE_CHANNELS", "").strip()
+        if _free_response:
+            self._free_response_channels = set(c.strip() for c in _free_response.split(",") if c.strip())
+        else:
+            self._free_response_channels = set(
+                str(c) for c in extra.get("free_response_channels", []) if str(c).strip()
+            )
+
+        # Auto-thread: by default, Chatto creates a thread for replies to room
+        # messages (not DMs, not already in a thread). This keeps conversations
+        # organized in the room. Can be disabled via extra.auto_thread=false.
+        self._auto_thread = utils.is_truthy_value(os.getenv("CHATTO_AUTO_THREAD", "").strip().lower()) # todo: add config.yaml support
+
+
+
+

+ 42 - 10
test_adapter.py

@@ -22,14 +22,12 @@ from collections import OrderedDict
 import pytest
 import pytest_asyncio
 
-# Lazy imports for chattolib (required by Hermes plugin system)
-from tools.lazy_deps import lazy_import
+# Import chattolib types for tests - using vendored chattolib from adapter
 
 # -- Path setup --
 sys.path.insert(0, "/opt/hermes")
 sys.path.insert(0, "/root/.hermes/plugins/platforms/chatto")
 
-import adapter as chatto_adapter
 from adapter import (
     _EMOJI_TO_SHORTCODE,
     _MAX_MESSAGE_LENGTH,
@@ -40,7 +38,7 @@ from adapter import (
     register,
 )
 
-from gateway.config import Platform, PlatformConfig
+from gateway.config import PlatformConfig
 from gateway.platforms.base import SendResult, MessageEvent, MessageType
 
 
@@ -298,8 +296,17 @@ class TestUserLookup:
         return adapter
 
     async def test_get_user_calls_chattolib(self, adapter):
-        User = lazy_import("chattolib.types", "User")
-        GetUserResponse = lazy_import("chattolib.types", "GetUserResponse")
+        # Import chattolib types for testing (try vendored first)
+        try:
+            from chattolib_vendor.chattolib.types import User, GetUserResponse
+        except ImportError:
+            try:
+                from chattolib.types import User, GetUserResponse
+            except ImportError:
+                # Fallback to mocked types if chattolib not installed
+                from unittest.mock import MagicMock
+                User = MagicMock
+                GetUserResponse = MagicMock
         mock_user = User(id="user-1", login="testuser", display_name="Test User")
         adapter._chatto_client.get_user.return_value = GetUserResponse(user=mock_user)
         result = await adapter.get_user("user-1")
@@ -308,8 +315,17 @@ class TestUserLookup:
         assert result["login"] == "testuser"
 
     async def test_get_user_caching(self, adapter):
-        User = lazy_import("chattolib.types", "User")
-        GetUserResponse = lazy_import("chattolib.types", "GetUserResponse")
+        # Import chattolib types for testing (try vendored first)
+        try:
+            from chattolib_vendor.chattolib.types import User, GetUserResponse
+        except ImportError:
+            try:
+                from chattolib.types import User, GetUserResponse
+            except ImportError:
+                # Fallback to mocked types if chattolib not installed
+                from unittest.mock import MagicMock
+                User = MagicMock
+                GetUserResponse = MagicMock
         mock_user = User(id="user-1", login="testuser", display_name="Test User")
         adapter._chatto_client.get_user.return_value = GetUserResponse(user=mock_user)
         result1 = await adapter.get_user("user-1")
@@ -381,7 +397,15 @@ class TestRoomOperations:
         return adapter
 
     async def test_create_room(self, adapter):
-        Room = lazy_import("chattolib.types", "Room")
+        # Import chattolib types for testing (try vendored first)
+        try:
+            from chattolib_vendor.chattolib.types import Room
+        except ImportError:
+            try:
+                from chattolib.types import Room
+            except ImportError:
+                from unittest.mock import MagicMock
+                Room = MagicMock
         mock_room = Room(id="room-123", name="Test Room", kind="ROOM_KIND_GROUP",
                         description="", archived=False, group_id="", universal=True)
         adapter._chatto_client.create_room.return_value = mock_room
@@ -390,7 +414,15 @@ class TestRoomOperations:
         adapter._chatto_client.create_room.assert_called_once()
 
     async def test_start_dm(self, adapter):
-        Room = lazy_import("chattolib.types", "Room")
+        # Import chattolib types for testing (try vendored first)
+        try:
+            from chattolib_vendor.chattolib.types import Room
+        except ImportError:
+            try:
+                from chattolib.types import Room
+            except ImportError:
+                from unittest.mock import MagicMock
+                Room = MagicMock
         mock_room = Room(id="dm-123", name="DM with user", kind="ROOM_KIND_DM",
                         description="", archived=False, group_id="", universal=False)
         adapter._chatto_client.start_dm.return_value = mock_room

+ 38 - 0
vendor_chattolib.sh

@@ -0,0 +1,38 @@
+#!/bin/bash
+# Vendoring script for hermes-chatto-plugin
+# Downloads dependencies from pyproject.toml using uv and vendors them into chattolib_vendor/
+# Usage: ./vendor_chattolib.sh
+
+set -euo pipefail
+
+PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+echo "Vendoring dependencies from pyproject.toml using uv..."
+
+# Clean up any existing vendored dependencies
+rm -rf "${PLUGIN_DIR}/vendor"
+
+# Create target directory
+mkdir -p "${PLUGIN_DIR}/vendor"
+
+# Create a temporary virtual environment
+uv venv .venv --clear
+
+# Set environment variables for the venv
+# shellcheck disable=SC1091
+source .venv/bin/activate
+cd "${PLUGIN_DIR}"
+
+uv pip install chattolib --target vendor-tmp
+
+cp -r vendor-tmp/chattolib* "${PLUGIN_DIR}/vendor/"
+
+# Create __init__.py to make it importable
+cat > "${PLUGIN_DIR}/vendor/chattolib/__init__.py" << 'EOF'
+# Vendored chattolib
+EOF
+
+rm -rf vendor-tmp/
+
+echo "✓ Dependencies vendored successfully to ${PLUGIN_DIR}/vendor/chattolib/"
+echo "do 'git add vendor/' now or later"

Некоторые файлы не были показаны из-за большого количества измененных файлов