COLLECTORS-SPEC — Yandex Market (8 коллекторов)
Детали механики вызова YM Partner API. Общие паттерны — в COLLECTORS-SPEC.md.
Field-mapping'и — в data-model/intake-ym.md. Пути file:line — от
marketplace-collector-v3/collectors/.
Общее: base = YANDEX_MARKET["base_url"] = https://api.partner.market.yandex.ru; авторизация —
заголовок Api-Key (Bearer не используется). Два уровня API:
- business-level (
YANDEX_MARKET["business_id"]) — offer_mappings, prices, promos, analytics, storage; всегда mp_id=6; - campaign-level (
YANDEX_MARKET["campaigns"]={model: campaign_id}; fbs=22027612 → mp6, fby=61824486 → mp8) — stats_orders, stocks, returns; развод mp_id через_MODEL_TO_MP = {"fbs": MP_YM_FBS(6), "fby": MP_YM_FBY(8)}и инъекцию_v3_mp_idв каждый item (ym_stats_orders.py:26, :71-72, запись :153).
Постраничная пагинация везде одинакова: params={"limit": N} + page_token; следующий токен из
result.paging.nextPageToken; стоп — токена нет; bounded _MAX_PAGES (epic-10/04). Пример:
next_token = paging.get("nextPageToken") # ym_offer_mappings.py:64-67
if not next_token:
break
page_token = next_token
1. ym.offer_mappings — мэппинги офферов
YmOfferMappingsCollector, mp_id=6 (ym_offer_mappings.py:22-24, business-level). Dual-write вручнуюmappings/ym/offer_mappings.yaml(:17, :110).- POST
/businesses/{business_id}/offer-mappings(:43), body{}(:49). Снапшот, окна нет. - Пагинация: limit=200 (:46), page_token (:63-67),
_MAX_PAGES=500(:19). - Таблица:
gwptd_intake.ym_offer_mappings(:74, :94).
2. ym.stats_orders — заказы (обе кампании)
YmStatsOrdersCollector, mp_id default=6, фактический per-row из модели кампании (ym_stats_orders.py:29-31, :26, :71-72, :153). Dual-write вручнуюmappings/ym/stats_orders.yaml(:21, :170).- POST
/v2/campaigns/{campaign_id}/stats/ordersдля каждой кампании изYANDEX_MARKET["campaigns"].items()(:50-51), body{"dateFrom": today-7, "dateTo": today}(:46-48). - Пагинация: limit=200 (:55) + page_token (:56-57, :75-79),
_MAX_PAGES=500(:23). - Ловушка:
creationDateприходит в трёх форматах — ISO сT,DD-MM-YYYY HH:MM:SS, date-only — нормализация (:116-140). - Таблица:
gwptd_intake.ym_stats_orders(:86, :146).
3. ym.stocks — остатки (обе кампании)
YmStocksCollector, mp_id per-campaign (ym_stocks.py:23-25, :20, :60-63, :115). Dual-write вручнуюmappings/ym/stocks.yaml(:15, :127).- POST
/v2/campaigns/{campaign_id}/offers/stocks(:41), body{}(:48). Снапшот. - Пагинация: limit=200 (:46), page_token (:68-71),
_MAX_PAGES=500(:17). - Offer вложен в warehouse — итерируются склады, в item инъецируются
_v3_warehouse_name/id(:56-64);stocks[]разбирается по типам в счётчики AVAILABLE/FIT/FREEZE/DEFECT/EXPIRED (:101-105). - Таблица:
gwptd_intake.ym_stocks(:78, :108).
4. ym.prices — цены (business-level)
YmPricesCollector, mp_id=6 (ym_prices.py:30-32) — цены едины для FBS/FBY, хранятся под mp6 как канонический фид (:4-6).legacy_mapping_path = mappings/ym/prices.yaml(:34) →mp_prices_dailyчерез BaseCollector (:105).- POST
/v2/businesses/{business_id}/offer-prices(:47), body{}(:54). Снапшот. - Пагинация: limit=200 (:50), page_token (:68),
_MAX_PAGES=100(:16, меньше остальных). - Таблица:
gwptd_intake.ym_prices(:77, :93). Пишетprice.value,discountBase,currencyId(:100-103).
5. ym.returns — возвраты (обе кампании, F-15)
YmReturnsCollector, mp_id per-campaign (ym_returns.py:24-26, :21, :60-61, :103). Dual-write вручнуюmappings/ym/returns.yaml(:16, :116).- Единственный GET среди кампанийных:
/v2/campaigns/{campaign_id}/returns(:44, :51); окно — query-параметрыfromDate/toDateв формате%d-%m-%Y,today−30 … today(:41-42, :48). - Пагинация: limit=50 (не 200, :48);
page_token, причёмpaging— на ВЕРХНЕМ уровне ответа, не вresult(:63-64);_MAX_PAGES=500(:18). - F-15 (DEFECT-LEDGER): дата возврата —
submittedDateприоритетнееupdateDate(updateDate дрейфует при смене статуса); accrual-семантика по BR-003 (:93-95).refundAmount.valueчитается только если dict (:111). - Таблица:
gwptd_intake.ym_returns(:73, :97).
6. ym.promos — акции (business-level)
YmPromosCollector, mp_id=6 (ym_promos.py:35-37).legacy_mapping_path = mappings/ym/promos.yaml(:39) →mp_promotionsчерез BaseCollector.- POST
/v2/businesses/{business_id}/promos(:52), body{}(:53). Без пагинации — один запрос, весь списокresult.promos(:58-63). Снапшот видимых промо на момент сбора (:3-5). - Таблица:
gwptd_intake.ym_promos(:72, :92). ИзmechanicsInfo.type,assortmentInfo.activeOfferCount/potentialOfferCount(:103-107).
7. ym.analytics — воронка shows-sales (async xlsx)
YmAnalyticsCollector, mp_id=6 (ym_analytics.py:85-87).legacy_mapping_path = mappings/ym/analytics.yaml(:89) →mp_analytics_daily(:293).self.use_cache = False(:95) — «async несовместим с HTTP-кэшем: закэшированный PROCESSING зациклил бы ожидание» (:93-94).- Async 3-step:
- generate: POST
/v2/reports/shows-sales/generate, body{dateFrom: today-7, dateTo: today-1, grouping: "OFFERS", businessId}(:125-135) →result.reportId(:141). Окно по вчера — «YM не отдаёт сегодняшний день» (:10, :125-126); - poll: GET
/v2/reports/info/{report_id}доstatus=="DONE"→result.fileURL (:148, :157-159);FAILED→ выход (:160-162)._POLL_ATTEMPTS=30×_POLL_SLEEP=10 с(:31-32, sleep в начале попытки :151); - download:
self.session.get(file_url, timeout=120)— сырые байты НАПРЯМУЮ через session, мимоself.http()(тот JSON-only); Api-Key уже в headers (:168-171); статус скачивания вручную вrun.record_http(:175-176).
- generate: POST
- Парсинг xlsx:
openpyxl.load_workbook(read_only=True, data_only=True)(:195); обязательныйws.reset_dimensions()— YM-xlsx без корректного<dimension>, иначе заголовок схлопывается в одну ячейку (:196-200). Заголовки однорядные русские; маппинг заголовок→индекс (:210-225); без колонки «Ваш SKU» — abort (:216-221). Метрики: Показы/Клики/Добавления в корзину/Заказанные товары/суммы/Доставлено (:43-52, :240-251). Даты_normalize_dayDD-MM-YYYY/YYYY-MM-DD (:73-82). - F-31 — архив xlsx:
_XLSX_ARCHIVE_DIR = os.environ.get("V3_XLSX_ARCHIVE_DIR", "/opt/gwptd-analytics/xlsx-archive")(:34-38) — каталог ВНЕ git-репо (dirty worktree ломал автопулл);_archive_xlsx()fail-soft, имя{endpoint_code}_{YYYYmmdd_HHMMSS}.xlsx(:99-116); вызов ДО парсинга (:182). Мотив: в raw_payload только распарсенные строки — при смене русских заголовков без бинарника отчёт невосстановим (:34-37, :181). Temp-файлNamedTemporaryFile, удаляется вfinally(:185-258). - Таблица:
gwptd_intake.ym_analytics(:263, :279).
8. ym.storage — хранение/услуги (async xlsx, двухрядные заголовки)
YmStorageCollector, mp_id=6 (ym_storage.py:105-107).legacy_mapping_path = mappings/ym/storage.yaml(:109) →mp_storage_costs.self.use_cache = False(:113-115, «см. wb_paid_storage»). Rate limit отчёта: 100 req/hour; объём ~49 строк/мес (:10-11) — темп задаёт poll-sleep 10 с (base.py:78-79).- Async 3-step:
- generate: POST
/v2/reports/united-marketplace-services/generate?format=FILE, body{businessId, dateTimeFrom: "{today-30}T00:00:00+03:00", dateTimeTo: "{today-1}T23:59:59+03:00", placementPrograms: ["FBS","FBY"]}(:151-160) →result.reportId(:166); - poll: GET
/v2/reports/info/{report_id}доDONE/FAILED(:174, :182-187);_POLL_ATTEMPTS=30×_POLL_SLEEP=10(:29-30, :176); - download:
session.get(file_url, timeout=_DOWNLOAD_TIMEOUT=120)мимоhttp()(:31, :193-195).
- generate: POST
- Парсинг xlsx: сканируются ВСЕ листы (:228) — реальный отчёт = 13 листов по типам услуг,
отдельного листа «Хранение невыкупов и возвратов» нет (:41-47). Двухрядные заголовки:
строка-группа сверху + строка с «Стоимость услуги, ₽», ищется в первых 8 строках (:239-246);
колонки — по русским алиасам (amount :48; дата :49-53; Услуга/SKU/номер заказа/номер
возврата/количество/тип записи :54-59, :260-267). Обязательный
ws.reset_dimensions()(:232). - Фильтр строк: keywords
("хранен","невыкуп","возврат")по имени листа или тексту услуги (:63, :272-296); пропуск «итого» (:287-288) и amount==0 (:283)._clean_idотрезает.0-хвост numeric-ID (:66-76); даты DD.MM.YYYY/YYYY-MM-DD (:79-88). offer_id = sku or return_id or order_id(:303);warehouse_name=None,cost_type="storage",amount=abs(...)(:311-313). source_key ={sheet}:{date}:{offer_id}:{return_id|order_id}— уникален по строке, иначе legacy-upsert(mp_id,date,offer,wh,type)схлопнул бы разные начисления (:325-328).- F-31 — архив xlsx: тот же
V3_XLSX_ARCHIVE_DIR+ fail-soft_archive_xlsx(:33-37, :119-136), вызов до парсинга (:205). - Таблица:
gwptd_intake.ym_storage(:345, :364).
Сводка по осям
- fbs/fby разводятся только в campaign-level коллекторах (№2, 3, 5) через
_v3_mp_id; business-level (№1, 4, 6, 7, 8) — всегда mp6 (у storage при этом в запросе обе программы FBS+FBY). - F-31 (архив xlsx) — только №7, №8. F-15 — только №5. use_cache=False — только async №7, №8.
- Dual-write через BaseCollector (
legacy_mapping_path) — №4, 6, 7, 8; вручную — №1, 2, 3, 5.