27 KiB
Build 008 — Dzengi REST Adapter
Статус: Завершён
Подсистема: market_data/acquisition
Область: Instrument Reference Data
Тип изменения: Изолированное добавление transport adapter без подключения к production runtime
Результат полного набора тестов: 123 passed
1. Цель Build 008
Цель Build 008 — реализовать конкретный транспортный источник Instrument Reference Data для Dzengi REST API, соответствующий созданному в Build 007 протоколу:
class InstrumentDocumentSource(Protocol):
def fetch_instrument_document(self) -> object:
...
Реализован класс:
DzengiInstrumentDocumentSource
Архитектурная граница Build 008:
Dzengi REST API
↓
ExchangeRestClient
↓
DzengiInstrumentDocumentSource
↓
object
Build 008 отвечает только за получение декодированного транспортного документа.
Он не выполняет его структурную проверку, parsing, проверку значений или преобразование во внутренние модели Instrument.
2. Почему Build 008 выполняется именно сейчас
До начала Build 008 были завершены:
Build 001 — внутренняя модель Instrument Reference Data
Build 002 — raw-модели ответа Dzengi
Build 003 — структурная валидация exchangeInfo
Build 004 — parser exchangeInfo
Build 005 — value validation
Build 006 — mapper Dzengi → Instrument
Build 007 — Protocol и Exceptions
После Build 007 были зафиксированы контракты будущей orchestration-цепочки:
InstrumentDocumentSource
↓
InstrumentDocumentHandler
↓
InstrumentFeedProtocol
↓
Registry
↓
Acquisition Service
Следующим необходимым шагом стала конкретная реализация первого контракта:
InstrumentDocumentSource
для источника Dzengi.
3. Анализ существующей реализации
Перед написанием кода были проанализированы:
app/src/integrations/exchange/rest_client.py
app/src/integrations/exchange/exceptions.py
app/src/core/config.py
app/src/integrations/exchange/service.py
app/src/market_data/acquisition/adapters/dzengi/rest.py
Также выполнен поиск существующих вызовов exchangeInfo:
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "exchangeInfo|exchange_info|get_exchange_symbols" \
src/integrations/exchange
Было подтверждено, что legacy-реализация использует:
client.get_json("/api/v1/exchangeInfo")
в методе:
ExchangeService.get_exchange_symbols()
Таким образом, фактически используемый работающим ботом endpoint:
/api/v1/exchangeInfo
4. Принятое решение по HTTP transport
В Build 008 не создавалась новая параллельная HTTP-реализация.
Вместо этого новый Dzengi REST Adapter временно переиспользует существующий:
src.integrations.exchange.rest_client.ExchangeRestClient
Причины:
клиент уже использует актуальный EXCHANGE_BASE_URL;
клиент уже применяет EXCHANGE_TIMEOUT_SEC;
клиент уже реализует HTTP GET;
клиент уже устанавливает необходимые HTTP headers;
клиент уже выполняет JSON decoding;
клиент уже преобразует HTTP/network ошибки в exchange exceptions;
создание второго HTTP transport дублировало бы существующую инфраструктуру;
legacy-код работающего бота не требуется изменять.
Это является временной переходной зависимостью.
Условие её будущего удаления:
появление утверждённого общего transport-клиента
или
полный вывод legacy integrations/exchange из эксплуатации.
5. Изменённые файлы
В рамках Build 008 изменён production-файл:
app/src/market_data/acquisition/adapters/dzengi/rest.py
Создан тестовый файл:
app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
Другие production-файлы не изменялись.
6. Реализованный Dzengi REST Adapter
В файле:
app/src/market_data/acquisition/adapters/dzengi/rest.py
реализован класс:
class DzengiInstrumentDocumentSource:
...
Его публичный метод:
def fetch_instrument_document(self) -> object:
...
соответствует контракту:
InstrumentDocumentSource
7. Endpoint exchangeInfo
В adapter зафиксирован endpoint:
_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo"
Он соответствует фактически используемому legacy-кодом endpoint.
Путь вынесен в приватную константу, чтобы не дублировать строковый литерал внутри методов.
Build 008:
не меняет версию endpoint;
не вводит альтернативный endpoint;
не изменяет legacy-вызов;
не переключает production runtime.
8. Почему используется get_payload()
Legacy REST-клиент предоставляет два метода:
get_payload() -> object
и:
get_json() -> dict
Для нового adapter выбран:
get_payload()
Это принципиальное архитектурное решение.
Контракт InstrumentDocumentSource возвращает:
object
Следовательно, transport layer не должен утверждать, что полученный JSON обязательно является объектом dict.
Если внешний источник вернёт:
dict
list
str
int
float
bool
None
REST adapter должен вернуть декодированный документ следующему слою без структурной интерпретации.
Проверка структуры относится к Build 003:
validate_exchange_info_schema()
Поэтому архитектурная граница остаётся:
Dzengi REST API
↓
ExchangeRestClient.get_payload()
↓
object
↓
Schema Validation
Использование get_json() преждевременно смешало бы:
transport responsibility
и:
schema validation responsibility
9. Отсутствие преобразования документа
DzengiInstrumentDocumentSource возвращает результат:
client.get_payload(_EXCHANGE_INFO_PATH)
без изменения.
Adapter не выполняет:
копирование документа;
изменение полей;
извлечение payload;
извлечение symbols;
нормализацию значений;
преобразование коллекций;
создание raw-моделей;
создание Instrument.
Тестами подтверждено сохранение identity:
assert result is document
как для dict, так и для list.
10. Dependency Injection
Adapter поддерживает передачу существующего REST-клиента:
class DzengiInstrumentDocumentSource:
def __init__(
self,
client: _PayloadRestClient | None = None,
) -> None:
self._client = client
Поведение:
client передан
↓
используется переданный объект
client не передан
↓
создаётся ExchangeRestClient()
Dependency injection необходим уже на текущем Build для:
unit-тестирования без реальных сетевых запросов;
точной проверки вызываемого endpoint;
эмуляции transport errors;
проверки неизменности возвращаемого документа.
11. Приватный транспортный Protocol
Первоначально конструктор adapter был аннотирован следующим образом:
client: ExchangeRestClient | None = None
При передаче тестового StubRestClient Pylance корректно обнаружил несовместимость типов:
Argument of type "StubRestClient" cannot be assigned
to parameter "client" of type "ExchangeRestClient | None"
Оставлять такую диагностическую ошибку в Build было признано неправильным.
Для исправления создан минимальный приватный структурный контракт:
class _PayloadRestClient(Protocol):
def get_payload(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> object:
...
Теперь конструктор принимает:
client: _PayloadRestClient | None = None
Это позволяет типобезопасно передавать:
ExchangeRestClient;
StubRestClient;
любую другую реализацию с совместимым get_payload().
12. Почему _PayloadRestClient является приватным
Protocol назван:
_PayloadRestClient
с ведущим подчёркиванием сознательно.
На текущем этапе он нужен только как внутренняя типовая граница конкретного Dzengi REST Adapter.
Он:
не является публичным контрактом всей acquisition subsystem;
не экспортируется через __init__.py;
не используется другими production-модулями;
не создаёт новый общий transport abstraction layer.
Если в будущем нескольким adapter потребуется общий HTTP transport contract, его выделение в публичный модуль должно выполняться отдельным архитектурным решением на основании реальной потребности.
13. Ленивое создание ExchangeRestClient
Если клиент не передан, он создаётся внутри:
fetch_instrument_document()
а не в конструкторе DzengiInstrumentDocumentSource.
Последовательность:
DzengiInstrumentDocumentSource()
↓
объект создаётся без немедленного создания ExchangeRestClient
fetch_instrument_document()
↓
создаётся ExchangeRestClient
↓
выполняется REST-запрос
Это важно, потому что конструктор legacy ExchangeRestClient:
загружает настройки;
проверяет EXCHANGE_BASE_URL;
может выбросить исключение ещё до HTTP-запроса.
Создание клиента внутри try гарантирует, что ошибка его создания также преобразуется в:
InstrumentReferenceTransportError
14. Transport error boundary
Build 007 добавил специализированную ошибку:
InstrumentReferenceTransportError
Build 008 впервые использует её в production-коде новой подсистемы.
Любая ошибка, возникающая внутри transport boundary:
except Exception as exc:
преобразуется в:
raise InstrumentReferenceTransportError(
"Не удалось получить Instrument Reference Data "
f"от Dzengi: {exc}"
) from exc
Это обеспечивает единый публичный тип transport-ошибки для новой acquisition subsystem.
15. Какие ошибки оборачиваются
Тестами подтверждена обработка:
ExchangeConnectionError
ExchangeResponseError
RuntimeError
ошибки создания ExchangeRestClient
Также текущая граница перехватывает другие Exception, возникающие при получении документа.
Наружу новой подсистемы не должны непосредственно выходить legacy-типы:
ExchangeConnectionError
ExchangeResponseError
ExchangeError
На границе нового adapter они преобразуются в:
InstrumentReferenceTransportError
16. Exception chaining
При преобразовании ошибки сохраняется исходное исключение:
raise InstrumentReferenceTransportError(...) from exc
Благодаря этому:
wrapped_error.__cause__ is original_error
Тестами подтверждено сохранение исходной причины.
Это важно для:
диагностики;
логирования;
traceback;
будущего анализа transport failures.
17. Что REST Adapter не делает
Build 008 сознательно не выполняет:
schema validation;
parsing;
value validation;
mapping;
создание Instrument;
создание Handler;
создание Feed;
создание Registry;
создание Acquisition Service;
кэширование;
retry;
логирование;
формирование Telegram-сообщений;
проверку exchange_enabled;
production-подключение.
В adapter отсутствуют вызовы:
validate_exchange_info_schema()
parse_exchange_info()
validate_exchange_info_values()
map_dzengi_exchange_info_to_instruments()
18. Почему adapter не проверяет exchange_enabled
DzengiInstrumentDocumentSource является transport adapter.
Его ответственность:
получить документ от конкретного внешнего источника.
Решение о том:
разрешено ли приложению обращаться к бирже;
в каком режиме работает приложение;
нужно ли использовать mock;
нужно ли использовать cache;
когда именно запускать acquisition;
относится к orchestration или service layer.
Поэтому Build 008 не добавляет проверку:
exchange_enabled
и не реализует mock-поведение.
19. Почему adapter не использует format_exchange_error_for_user()
В legacy-модуле существует:
format_exchange_error_for_user()
Он предназначен для пользовательского представления ошибок.
REST adapter не является UI-слоем.
Поэтому он не должен:
формировать пользовательский текст;
знать о Telegram UI;
классифицировать ошибку для экрана;
принимать решение о повторной попытке.
Его задача — предоставить точную предметную ошибку:
InstrumentReferenceTransportError
20. Реализованные тестовые сценарии
Создан файл:
app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
Реализовано 10 тестов.
Проверены следующие сценарии:
DzengiInstrumentDocumentSourceсоответствуетInstrumentDocumentSource;- вызывается endpoint
/api/v1/exchangeInfo; - возвращаемый
dictпередаётся без изменения; - возвращаемый
listпередаётся без изменения; - injected client действительно используется;
ExchangeConnectionErrorпреобразуется вInstrumentReferenceTransportError;ExchangeResponseErrorпреобразуется вInstrumentReferenceTransportError;- неизвестная
RuntimeErrorтакже преобразуется в transport error; - ошибка создания
ExchangeRestClientпреобразуется в transport error; - adapter не преобразует wrapped document.
Дополнительно проверено:
исходное исключение сохраняется в __cause__;
текст исходной ошибки сохраняется в transport error;
реальный сетевой запрос в unit-тестах не выполняется.
21. Выполненные проверки
Проверка 1 — unit-тесты Dzengi REST Adapter
Команда:
python -m pytest \
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
-q
Результат:
.......... [100%]
10 passed in 0.02s
Статус:
PASSED
Проверка 2 — Python compilation
Команда:
python -m py_compile \
src/market_data/acquisition/adapters/dzengi/rest.py \
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
Результат:
Команда завершилась без ошибок и без вывода.
Статус:
PASSED
Проверка 3 — полный набор тестов проекта
Команда:
python -m pytest -q
Результат:
........................................................................................................................... [100%]
123 passed in 0.08s
Статус:
PASSED
Проверка 4 — отсутствие преждевременной production-интеграции
Команда:
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "DzengiInstrumentDocumentSource|_PayloadRestClient|InstrumentReferenceTransportError" \
src tests
Подтверждено:
DzengiInstrumentDocumentSource
используется только в новом Dzengi REST Adapter и его unit-тестах;
_PayloadRestClient
остаётся приватным контрактом внутри rest.py;
InstrumentReferenceTransportError
используется только внутри новой market_data/acquisition subsystem
и её unit-тестов.
Не обнаружено подключения к:
ExchangeService
Telegram UI
AutoTrade
Trading runtime
другим production-потребителям
Статус:
PASSED
22. Архитектура после Build 008
После завершения Build 008 новая часть подсистемы имеет следующую структуру:
market_data/
└── acquisition/
├── exceptions.py
│ ├── MarketDataAcquisitionError
│ ├── InstrumentReferenceTransportError
│ ├── InstrumentReferenceSchemaError
│ ├── InstrumentReferenceParseError
│ ├── InstrumentReferenceValueError
│ └── InstrumentReferenceMappingError
│
├── protocol.py
│ ├── InstrumentDocumentSource
│ ├── InstrumentDocumentHandler
│ └── InstrumentFeedProtocol
│
├── models/
│ └── instrument.py
│ └── Instrument
│
├── validation/
│ ├── schema.py
│ └── values.py
│
└── adapters/
└── dzengi/
├── models.py
├── parser.py
├── mapper.py
└── rest.py
├── _PayloadRestClient
└── DzengiInstrumentDocumentSource
На текущем этапе ещё не реализованы:
instrument_handler.py
instrument_feed.py
registry.py
service.py
23. Полная архитектурная цепочка после Build 008
Теперь реализован транспортный этап:
Dzengi REST API
↓
ExchangeRestClient.get_payload()
↓
DzengiInstrumentDocumentSource
↓
object
Уже реализован processing pipeline:
object
↓
validate_exchange_info_schema()
↓
ValidatedExchangeInfoDocument
↓
parse_exchange_info()
↓
DzengiExchangeInfoResponse
↓
validate_exchange_info_values()
↓
map_dzengi_exchange_info_to_instruments()
↓
tuple[Instrument, ...]
Однако эти две части пока намеренно не соединены.
Их соединение относится к:
Build 009 — Instrument Handler
24. Влияние на legacy-систему
Build 008 не подключён к существующим компонентам:
ExchangeService
ExchangeSymbol
SymbolValidationResult
Telegram UI
AutoTrade
Market Stream
Market Data Runner
Execution Quality
Не изменены:
ExchangeService.get_exchange_symbols()
ExchangeService.validate_symbol()
ExchangeService.get_symbol_runtime_status()
normalize_symbol()
symbol_candidates()
Старый production-путь продолжает работать без изменений.
Новый adapter существует изолированно и пока вызывается только unit-тестами.
25. Обратная совместимость
Подтверждено сохранение:
сигнатур существующих методов;
старых импортов;
существующего формата legacy-ошибок;
Telegram UI;
автоторговли;
runtime-поведения;
legacy ExchangeSymbol;
legacy-кэша.
Build 008 имеет полную обратную совместимость.
26. Классификация изменений
| Изменение | Классификация |
|---|---|
DzengiInstrumentDocumentSource |
Обязательное архитектурное изменение |
Переиспользование ExchangeRestClient |
Временная переходная зависимость |
_PayloadRestClient |
Необходимый приватный structural contract |
| Dependency injection клиента | Улучшение тестируемости и типизации |
Использование get_payload() |
Обязательное разделение transport и schema validation |
InstrumentReferenceTransportError в production adapter |
Обязательная граница ошибок новой подсистемы |
| Exception chaining | Улучшение диагностируемости |
| Изменение legacy REST-клиента | Отсутствует |
| Изменение production-поведения | Отсутствует |
27. Условие завершения Build 008
Все условия выполнены:
DzengiInstrumentDocumentSource
реализован;
InstrumentDocumentSource
соблюдён;
endpoint /api/v1/exchangeInfo
вызывается через ExchangeRestClient.get_payload();
сырой декодированный документ
возвращается без преобразования;
transport exceptions
преобразуются в InstrumentReferenceTransportError;
исходная причина ошибки
сохраняется через exception chaining;
unit-тесты
не выполняют реальных сетевых запросов;
полный pytest
проходит;
adapter
не подключён к production runtime.
28. Итог Build 008
Build 008 завершён успешно.
Реализовано:
DzengiInstrumentDocumentSource
_PayloadRestClient
transport error boundary
dependency injection REST-клиента
получение /api/v1/exchangeInfo через get_payload()
Подтверждено:
10 REST adapter tests passed
123 total project tests passed
Python compilation passed
No premature production integration detected
Legacy bot behavior unchanged
Итоговый статус:
BUILD 008 — COMPLETE
29. Следующий этап
Следующий этап утверждённого плана:
Build 009 — Instrument Handler
Его задача — соединить уже реализованный processing pipeline:
object
↓
validate_exchange_info_schema()
↓
parse_exchange_info()
↓
validate_exchange_info_values()
↓
map_dzengi_exchange_info_to_instruments()
↓
tuple[Instrument, ...]
в конкретную реализацию контракта:
InstrumentDocumentHandler
Предполагаемая архитектурная граница Build 009:
object
↓
Instrument Handler
↓
tuple[Instrument, ...]
Build 009 не должен:
самостоятельно выполнять HTTP-запросы;
создавать Instrument Feed;
создавать Registry;
создавать Acquisition Service;
изменять ExchangeService;
подключаться к production runtime.