简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

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

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

Обзор

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

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

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.Core import client

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

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

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/(имя_файла, файл)/(имя_файла, файл, тип_содержимого)}
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 Интервал в секундах для отправки心跳 (необязательно)

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

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-соединений

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

from ErisPulse.Core import lifecycle

@lifecycle.on("client.ws.connect")
async def on_ws_connect(event_data):
    print(f"WS соединение: {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="Нормальное закрытие")

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

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}")

Общее перехватывание

Для общего перехвата всех исключений HTTP/WS клиентских запросов можно использовать ClientError:

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

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

Рекомендации по лучшим практикам

  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 только в том случае, если требуется различать типы сообщений

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