Сетевой клиент
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, результат будет идентичен.
Рекомендуемые практики
- Предпочтение глобального клиента: получите глобальный одиночный экземпляр с помощью
from ErisPulse.Core import client, что облегчает управление и мониторинг фреймворком - Избегайте прямого импорта aiohttp: используйте
clientвместоaiohttp.ClientSession, что позволит легко заменить нижележащую реализацию без изменения кода. Старый код, использующий напрямую aiohttp, по-прежнему будет работать, оба способа могут сосуществовать - Использование системы исключений ErisPulse: при использовании
sdk.clientловитеClientError, а неaiohttp.ClientError, чтобы убедиться, что код не зависит от конкретной библиотеки HTTP. Код, использующий напрямую aiohttp, не затронут - Разумная настройка таймаутов: задавайте разумные значения таймаутов в зависимости от скорости ответа API, чтобы избежать длительных блокировок
- Использование механизма повторных попыток: включайте повторные попытки для нестабильных API, чтобы повысить надежность
- Мониторинг статистики запросов: используйте
sdk.client.statsили события жизненного циклаclient.requestдля мониторинга запросов - Использование высокого уровня методов WebSocket: предпочтительно использовать
iter_text/iter_jsonи т.д., только при необходимости различать типы сообщений используйтеiter_messages
Связанные документы
- Менеджер маршрутов - HTTP/WebSocket маршрутизация (WebSocketConnection сервера и клиент разделяют один и тот же базовый класс)
- Руководство по разработке адаптеров - Использование HTTP-клиента в адаптере
- Управление жизненным циклом - Наблюдение за событиями запросов