ErisPulse ErisPulse
Открыть интерактивную версию →
Разделы страницы
Статическое зеркало — интерактивная версия является основной Открыть интерактивную версию →

Сетевой клиент

Tip

Когда нужно читать эту статью: при необходимости исходящих HTTP / WebSocket-запросов. Если не нужно, пропустите, это не повлияет на использование других функций.

ErisPulse предоставляет единый сетевой клиент, объединяющий HTTP-запросы, WebSocket-соединения и управление пулом соединений. Модули и адаптеры должны использовать этот клиент, а не импортировать сторонние библиотеки, такие как aiohttp / httpx / requests.

Обзор

Основные функции сетевого клиента:

  • Единый интерфейс: предоставляет методы get / post / put / delete / patch / request
  • WebSocket-клиент: через ws_connect устанавливает клиентское WebSocket-соединение
  • Автоматическое логирование: все запросы автоматически записываются в логи и собираются статистика
  • Интеграция с жизненным циклом: каждый запрос вызывает событие client.request жизненного цикла, WebSocket-соединение вызывает событие client.ws.connect
  • Поддержка повторных попыток: можно настроить количество и интервал автоматических повторных попыток
  • Управление таймаутами: отдельные таймауты для подключения и запроса
  • Возможность повторного использования соединений: управление пулом соединений на основе aiohttp.ClientSession
  • Система исключений: исключения aiohttp автоматически преобразуются в исключения ErisPulse (система исключений ClientError)

Быстрый старт

HTTP-запросы

from ErisPulse.Core import client

# GET-запрос
resp = await client.get("https://httpbin.org/get")
data = await resp.json()
print(resp.status)  # 200

# POST-запрос
resp = await client.post(
    "https://httpbin.org/post",
    json={"key": "value"},
)
data = await resp.json()

WebSocket-соединение

from ErisPulse import client   # Глобальный импорт с версии 2.10+ (глубокие пути ErisPulse.Core.client по-прежнему совместимы)

ws = await client.ws_connect("wss://example.com/ws")

async for text in ws.iter_text():
    await ws.send_text(f"Echo: {text}")

Автоматическая регистрация исходящих соединений в пуле (с версии 2.10+)

Соединения, созданные через ws_connect, по умолчанию автоматически регистрируются в пуле соединений фреймворка (отключение возможно с помощью track=False). Можно просматривать их с помощью connections.list(owner=...), рассылать сообщения; при закрытии соединения или отключении удалённой стороны оно автоматически удаляется; при удалении модуля или остановке адаптера фреймворк автоматически закрывает все исходящие соединения, предотвращая утечки.

from ErisPulse import client, connections

ws = await client.ws_connect("wss://example.com/ws")
print(ws.id, ws.owner)   # Уже зарегистрировано: идентификатор соединения и владелец

# Если контекст owner недоступен (например, в обратном вызове из потока инструментов), явно указывайте владельца, чтобы при удалении он мог быть автоматически освобождён:
ws2 = await client.ws_connect("wss://example.com/ws2", owner="MyAdapter")

См. подробнее Пул соединений и рассылка.

HttpResponse

Все методы запросов возвращают объект HttpResponse:

from ErisPulse.Core import client

resp = await client.get("https://httpbin.org/get")

resp.status       # int - HTTP-код состояния (например, 200, 404)
resp.reason       # str | None - описание состояния (например, "OK")
resp.headers      # Заголовки ответа (без учёта регистра)
resp.content_type # str | None - Content-Type
resp.url          # Конечный URL (возможно, изменился из-за перенаправления)
resp.raw          # Нижележащий объект ответа (в настоящий момент aiohttp.ClientResponse)

# Чтение тела ответа
body = await resp.read()       # bytes
text = await resp.text()       # str
data = await resp.json()       # Декодирование JSON
text = await resp.text("gbk")  # Указание кодировки

Методы запросов

GET

from ErisPulse.Core import client

resp = await client.get(
    "https://api.example.com/users",
    params={"page": "1", "limit": "10"},
    headers={"Authorization": "Bearer token"},
)

POST

from ErisPulse.Core import client

# JSON-тело запроса
resp = await client.post(
    "https://api.example.com/users",
    json={"name": "Alice", "age": 30},
)

# Тело запроса в формате формы
resp = await client.post(
    "https://api.example.com/login",
    data={"username": "admin", "password": "123"},
)

# Сырые данные
resp = await client.post(
    "https://api.example.com/upload",
    data=b"raw bytes",
    headers={"Content-Type": "application/octet-stream"},
)

# Загрузка файлов (используя параметр files, без необходимости импорта aiohttp)
# Формат: {имя_поля: объект_файла/bytes/(имя_файла, файл)/(имя_файла, файл, content_type)}
resp = await client.post(
    "https://api.example.com/upload",
    data={"description": "Аватар"},            # Дополнительно можно передавать обычные поля формы
    files={
        "file": ("photo.png", open("photo.png", "rb"), "image/png"),
    },
)

# Упрощённый синтаксис: передача объекта файла напрямую
resp = await client.post(
    "https://api.example.com/upload",
    files={"file": open("photo.png", "rb")},
)

# Загрузка данных из памяти (без сохранения на диск)
import io

resp = await client.post(
    "https://api.example.com/upload",
    files={"file": ("data.txt", io.BytesIO(b"file content"), "text/plain")},
)

PUT / DELETE / PATCH

from ErisPulse.Core import client

resp = await client.put("https://api.example.com/users/1", json={"name": "Bob"})
resp = await client.delete("https://api.example.com/users/1")
resp = await client.patch("https://api.example.com/users/1", json={"age": 31})

Общий метод request

from ErisPulse.Core import client

resp = await client.request(
    "OPTIONS",
    "https://api.example.com/resource",
    headers={"Origin": "https://example.com"},
)

Параметры

Параметры HTTP-запроса

Параметр Тип Описание
url str URL запроса
params dict[str, str] Параметры запроса (опционально)
headers dict[str, str] Дополнительные заголовки запроса (опционально)
data Any Тело запроса (форма или сырые данные) (опционально)
json Any Тело запроса в формате JSON (опционально)
files dict[str, Any] Поля для загрузки файлов (опционально, автоматически формируется multipart/form-data)
timeout float Таймаут запроса (секунды) (опционально, переопределяет значение по умолчанию)
max_retries int Максимальное количество повторных попыток (опционально, переопределяет значение по умолчанию)

Параметры ws_connect

Параметр Тип Описание
url str URL сервера WebSocket
headers dict[str, str] Дополнительные заголовки запроса (опционально)
heartbeat float Интервал в секундах для отправки heartbeat-сообщений (опционально)

Таймауты и повторные попытки

from ErisPulse.Core import Client

# Создание клиента с пользовательскими таймаутами
client = Client(
    timeout=60,           # Общий таймаут запроса 60 секунд
    connect_timeout=5,    # Таймаут подключения 5 секунд
    max_retries=3,        # Автоматические повторные попытки при сбое 3 раза
    retry_delay=2,        # Интервал между повторными попытками 2 секунды
)

# Переопределение таймаута для конкретного запроса
resp = await client.get("https://slow-api.example.com/data", timeout=120)

Note

Класс клиента с версии 2.8.0 переименован в Client (имя свойства sdk.client осталось неизменным); старое имя HttpClient сохранено как совместимый псевдоним, старый код не требует изменений.

Настраиваемые заголовки по умолчанию

client = Client(
    headers={
        "Authorization": "Bearer token",
        "X-App-Id": "my-app",
    },
    user_agent="MyBot/1.0",
)

Статистика запросов

from ErisPulse.Core import client

# Просмотр статистики
stats = client.stats
# {"total_requests": 42, "total_errors": 1, "total_bytes_sent": 0, "total_bytes_received": 0}

# Сброс статистики
client.reset_stats()

События жизненного цикла

События HTTP-запросов

После завершения каждого запроса вызывается событие client.request, которое можно использовать для мониторинга:

from ErisPulse.Core import lifecycle

@lifecycle.on("client.request")
async def on_request(event_data):
    print(f"{event_data['method']} {event_data['url']} -> {event_data['status']} ({event_data['elapsed']}s)")

События WebSocket-соединений

После установления каждого WebSocket-соединения вызывается событие client.ws.connect:

from ErisPulse.Core import lifecycle

@lifecycle.on("client.ws.connect")
async def on_ws_connect(event_data):
    print(f"WebSocket-соединение: {event_data['url']}")

Контекстное управление

# Использование как контекстного менеджера, автоматически закрывает сессию
async with Client(timeout=30) as client:
    resp = await client.get("https://httpbin.org/get")
    data = await resp.json()

WebSocket-клиент

Создание WebSocket-клиентского соединения через client.ws_connect() возвращает объект ClientWebSocket. Клиент и сервер WebSocket разделяют один и тот же базовый класс WebSocketConnectionBase, и интерфейсы send/receive/iter полностью совпадают.

Основное использование

from ErisPulse.Core import client

ws = await client.ws_connect("wss://example.com/ws", heartbeat=30)

await ws.send_text("Hello")
await ws.send_bytes(b"\x00\x01\x02")
await ws.send_json({"type": "ping"})

Получение сообщений

Рекомендуемые методы высокого уровня

Автоматически фильтруют типы сообщений, при разрыве соединения выбрасывают WebSocketDisconnect:

from ErisPulse.Core import client
from ErisPulse.Core.Bases.errors import WebSocketDisconnect

ws = await client.ws_connect("wss://example.com/ws")

# Получение одного сообщения
text = await ws.receive_text()    # str
data = await ws.receive_bytes()   # bytes
obj = await ws.receive_json()     # dict / list

# Итерация по сообщениям (автоматически останавливается при разрыве соединения)
async for text in ws.iter_text():
    print(text)

async for data in ws.iter_bytes():
    print(data)

async for obj in ws.iter_json():
    print(obj)

Низкоуровневые методы

Использование receive() и iter_messages() для обработки необработанных типов сообщений, можно различать TEXT / BINARY / CLOSE / ERROR:

from ErisPulse.Core import client
from ErisPulse.Core.Bases.websocket import WSMessage

ws = await client.ws_connect("wss://example.com/ws")

# Получение одного сообщения
msg = await ws.receive()
# msg.type  -> WSMessage.TEXT / WSMessage.BINARY / WSMessage.CLOSE / WSMessage.ERROR
# msg.data  -> str | bytes | None

# Итерация по сообщениям (автоматически останавливается при CLOSE/ERROR)
async for msg in ws.iter_messages():
    if msg.type == WSMessage.TEXT:
        print(f"Текст: {msg.data}")
    elif msg.type == WSMessage.BINARY:
        print(f"Бинарные данные: {len(msg.data)} байт")

WSMessage

WSMessage — это единый тип WebSocket-сообщения, независимый от нижележащей библиотеки:

Свойство Тип Описание
type str Тип сообщения: WSMessage.TEXT / WSMessage.BINARY / WSMessage.CLOSE / WSMessage.ERROR
data Any Данные сообщения

Свойства ClientWebSocket

Свойство Тип Описание
url URL URL соединения
headers Headers Заголовки ответа
closed bool Закрыто ли соединение
raw object Нижележащий объект (aiohttp.ClientWebSocketResponse)

Хуки жизненного цикла

Аналогично WebSocketConnection сервера, поддерживаются обратные вызовы on_disconnect и on_error:

from ErisPulse.Core import client

ws = await client.ws_connect("wss://example.com/ws")

@ws.on_disconnect
async def handle_disconnect(ws, reason="unknown"):
    print(f"Соединение разорвано: {reason}")

@ws.on_error
async def handle_error(ws, error=""):
    print(f"Ошибка соединения: {error}")

Закрытие соединения

await ws.close(code=1000, reason="Normal closure")

Система исключений

ErisPulse определяет единый иерархический набор исключений, и при запросах, инициированных через sdk.client, исключения aiohttp автоматически преобразуются в исключения ErisPulse.

Обратная совместимость: модули/адаптеры, использующие напрямую aiohttp.ClientSession, не затронуты. Преобразование исключений происходит только при запросах через sdk.client, код, использующий напрямую aiohttp, по-прежнему будет ловить исключения aiohttp.ClientError и т.д. Эти два способа могут сосуществовать.

Иерархия исключений

ErisPulseError
├── ClientError                  # Базовый класс для всех исключений HTTP/WS-клиента
│   ├── ClientConnectionError    # Ошибка подключения (ошибка DNS, отказ в подключении, недоступность сети)
│   ├── ClientTimeoutError       # Ошибка таймаута подключения или запроса
│   └── HTTPStatusError          # Ошибка HTTP 4xx/5xx
└── WebSocketError               # Базовый класс исключений WebSocket
    └── WebSocketDisconnect      # Исключение разрыва WebSocket-соединения (общее для клиента и сервера)

Обработка исключений

from ErisPulse.Core import client
from ErisPulse.Core.Bases.errors import (
    ClientError,
    ClientConnectionError,
    ClientTimeoutError,
    HTTPStatusError,
    WebSocketDisconnect,
    WebSocketError,
)

# Обработка исключений HTTP-запросов
try:
    resp = await client.get("https://api.example.com/data")
    data = await resp.json()
except ClientConnectionError:
    print("Невозможно подключиться к серверу")
except ClientTimeoutError:
    print("Таймаут запроса")
except ClientError as e:
    print(f"Запрос не удался: {e}")

# Обработка исключений WebSocket
try:
    ws = await client.ws_connect("wss://example.com/ws")
    async for text in ws.iter_text():
        await ws.send_text(f"Echo: {text}")
except WebSocketDisconnect as e:
    print(f"Соединение разорвано: code={e.code}, reason={e.reason}")
except WebSocketError as e:
    print(f"Ошибка WebSocket: {e}")

Общая обработка

Использование ClientError для общей обработки всех исключений HTTP/WS-клиента:

from ErisPulse.Core.Bases.errors import ClientError

try:
    resp = await client.get("https://api.example.com/data")
except ClientError as e:
    print(f"Ошибка клиента: {e}")

HTTPStatusError

Если необходимо проверить код состояния после запроса и выбросить исключение, можно использовать HTTPStatusError вручную:

from ErisPulse.Core.Bases.errors import HTTPStatusError

resp = await client.get("https://api.example.com/data")
if resp.status >= 400:
    raise HTTPStatusError(resp.status, await resp.text())

Использование в адаптере

Адаптер может использовать глобальный клиент или создать экземпляр клиента для отправки платформенных API-запросов:

from ErisPulse.Core import client
from ErisPulse.Core.Bases import BaseAdapter
from ErisPulse.Core.Bases.errors import ClientError

class MyAdapter(BaseAdapter):
    async def call_api(self, endpoint, **params):
        try:
            resp = await client.post(
                f"https://api.platform.com/{endpoint}",
                json=params,
                headers={"Authorization": f"Bearer {self.token}"},
            )
            return await resp.json()
        except ClientError as e:
            self.logger.error(f"Ошибка вызова API: {e}")
            raise

Также можно использовать from ErisPulse import sdk и sdk.client, результат будет идентичен.

Рекомендуемые практики

  1. Предпочтение глобального клиента: получите глобальный одиночный экземпляр с помощью from ErisPulse.Core import client, что облегчает управление и мониторинг фреймворком
  2. Избегайте прямого импорта aiohttp: используйте client вместо aiohttp.ClientSession, что позволит легко заменить нижележащую реализацию без изменения кода. Старый код, использующий напрямую aiohttp, по-прежнему будет работать, оба способа могут сосуществовать
  3. Использование системы исключений ErisPulse: при использовании sdk.client ловите ClientError, а не aiohttp.ClientError, чтобы убедиться, что код не зависит от конкретной библиотеки HTTP. Код, использующий напрямую aiohttp, не затронут
  4. Разумная настройка таймаутов: задавайте разумные значения таймаутов в зависимости от скорости ответа API, чтобы избежать длительных блокировок
  5. Использование механизма повторных попыток: включайте повторные попытки для нестабильных API, чтобы повысить надежность
  6. Мониторинг статистики запросов: используйте sdk.client.stats или события жизненного цикла client.request для мониторинга запросов
  7. Использование высокого уровня методов WebSocket: предпочтительно использовать iter_text / iter_json и т.д., только при необходимости различать типы сообщений используйте iter_messages

Связанные документы