"""avito_shared.py — общие константы/хелперы для avito*.py модулей. Единая точка правды для: - `RUS_MONTHS` — рус. месяц (родительный падеж) → номер, для парсинга дат вида "9 октября 2025" (было продублировано в avito.py/avito_detail.py/avito_houses.py). - `_unix_to_date` — unix-epoch (в СЕКУНДАХ) → `date`, в таймзоне Europe/Moscow. TZ-обоснование (issue #2129): было 3 независимые копии конвертации unix→date с РАЗНЫМИ timezone — MSK (avito.py, документировано issue #726), UTC (avito_houses.py), naive/system-local (avito_imv.py). Один и тот же epoch давал разные `date` в SERP / houses / IMV путях для одного и того же объявления. Почему MSK — канонический выбор (не UTC, не local): - Avito — российская площадка, отображает даты объявлений (`item-date` на SERP, `publishDate`, история цен `startPriceDate`/`lastPriceDate`/`removedDate`) в Europe/Moscow. Объявление, поднятое в 00:00–03:00 MSK, при наивной UTC-конвертации получило бы дату на день раньше, чем видно на самом Avito (обоснование #726). - avito_houses.py и avito_imv.py используются на одних и тех же данных (`housePlacementHistory` widget / IMV `placementHistory` — идентичные поля startPriceDate/lastPriceDate/removedDate, см. fixtures/avito_imv_getdata.json) — единственный смысл держать их согласованными с уже принятым MSK-решением из #726. - Проверено на живой fixture: startPriceDate/lastPriceDate — всегда полночь UTC (00:00:00), поэтому MSK vs UTC не меняет date для них. `removedDate`/`publishDate` несут реальное время суток — там UTC/naive-local и MSK ДЕЙСТВИТЕЛЬНО расходятся (пример из fixture: removedDate=1756077885 → UTC 2025-08-24, MSK 2025-08-25). Это единственный реальный сдвиг данных от этого фикса: `avito_imv_evaluations` / `house_placement_history`, поля `removed_date`/`publish_date`, у ~12.5% строк (события в 21:00–23:59 UTC/local) дата сдвинется на +1 день при следующем прогоне скрейпера. Схему/уже сохранённые строки не трогаем — это единственно правильное представление на будущее. - Fixed-offset `timezone(timedelta(hours=3))`, а не `zoneinfo("Europe/Moscow")` — не зависит от системной tzdata в контейнере; Москва не переходит на DST с 2014, так что fixed offset корректен всегда. """ from __future__ import annotations import logging from datetime import date, datetime, timedelta, timezone logger = logging.getLogger(__name__) # Europe/Moscow, UTC+3, без перехода на летнее время (с 2014). MSK = timezone(timedelta(hours=3)) # Русские месяцы (родительный падеж) для парсинга дат вида "9 октября 2025". RUS_MONTHS: dict[str, int] = { "января": 1, "февраля": 2, "марта": 3, "апреля": 4, "мая": 5, "июня": 6, "июля": 7, "августа": 8, "сентября": 9, "октября": 10, "ноября": 11, "декабря": 12, } def _unix_to_date(ts: int | float | None) -> date | None: """Конвертирует unix-epoch (в СЕКУНДАХ) в `date`, в таймзоне Europe/Moscow (MSK). Возвращает `None` для `None`/`0` (Avito использует 0 как sentinel «нет даты») и для значений, которые не конвертируются в валидную дату. ВАЖНО: ожидает epoch в СЕКУНДАХ. Avito SERP-поле `sortTimeStamp` отдаёт epoch в МИЛЛИСЕКУНДАХ — вызывающий код обязан поделить на 1000 до вызова (см. `avito.py::_build_sort_timestamp_map`). """ if not ts: return None try: return datetime.fromtimestamp(float(ts), tz=MSK).date() except (OSError, OverflowError, ValueError): logger.warning("Не удалось конвертировать unix timestamp: %r", ts) return None