24 KiB
type, work_item_id, title, version, status, created_at, updated_at, authors, related
| type | work_item_id | title | version | status | created_at | updated_at | authors | related | ||
|---|---|---|---|---|---|---|---|---|---|---|
| trz | ET-009 | ТЗ: Новые источники GPS-треков — EnduroRussia и Wikiloc | 1 | draft | 2026-06-01 | 2026-06-01 |
|
|
ТЗ — ET-009: Новые источники GPS-треков — EnduroRussia и Wikiloc
1. Терминология
- Source — внешний поставщик GPS-треков, описан записью в
config/gps_sources.yaml. Реализуется python-классом-наследникомSourceParserвsrc/api/gps_tracks/sources/<source_id>.py. - Region — географическая область сбора, описана записью в
config/gps_regions.yaml. Содержитbboxи список активныхsourcesдля этой области. - Pipeline-guard — проверка
_check_license_adr()вscripts/gps_collect.py, которая блокирует загрузку source-парсера если его ADR вlicense_adrимеетstatus != 'accepted'. - Activity-mapping — словарь
MAPPINGв каждом parser-модуле, переводящий внутренние категории источника в каноничныеACTIVITY_TYPES(src/api/gps_tracks/models.py). - Dedup-key — детерминированный ключ, по которому треки из разных
источников сливаются в одну запись (реализация в
src/api/gps_tracks/dedup.py:compute_dedup_key, ET-008). - Graceful-stop — поведение Wikiloc-парсера при HTTP 403/429:
returnиз async-генератора безraise, что приводит к статусу прогонаpartialилиrate_limitedбез падения процесса.
2. Архитектурные опоры из ET-008
ET-009 не строит новых модулей. Используются:
src/api/gps_tracks/sources/base.py:SourceParser— базовый класс.src/api/gps_tracks/sources/enduro_russia.py:EnduroRussiaParser— реализован.src/api/gps_tracks/sources/wikiloc.py:WikilocParser— реализован.scripts/gps_collect.py— оркестратор pipeline, поддерживает per-source rate-limit, licensing-guard, dedup, upsert.src/api/gps_tracks/db.py:upsert_track— merge поdedup_key, объединениеsourcesиexternal_urls.src/api/gps_tracks/endpoint.py—/api/gps-tracks,/api/gps-tracks/tiles/{z}/{x}/{y}.mvt,/api/gps-tracks/health.src/web/gps_tracks.js(или эквивалент в ET-008) — клиентский слой с динамическим фильтром источников.
ET-009 = конфиг + фикстуры + тесты + продакшн-прогон.
3. Требования
REQ-F-01 — Конфиг: enduro_russia.base_url
Файл config/gps_sources.yaml, запись с id: enduro_russia, поле
base_url устанавливается в https://endurorussia.ru (без дефиса).
Текущее значение https://enduro-russia.ru (с дефисом) считается
багом и должно быть заменено.
Acceptance check. После правки:
grep "base_url" config/gps_sources.yaml | grep enduro
выводит base_url: "https://endurorussia.ru".
REQ-F-02 — Конфиг: enduro_russia.enabled
В той же записи enabled: true.
Acceptance check. В config/gps_sources.yaml строка enabled: true
находится непосредственно под id: enduro_russia.
REQ-F-03 — Конфиг: запись wikiloc
В config/gps_sources.yaml добавляется новая запись с полями:
- id: wikiloc
name: "Wikiloc"
enabled: true
license_adr: "docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md"
base_url: "https://www.wikiloc.com"
rate_limit_sec: 10
user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
attribution: "© Wikiloc contributors"
parser_module: "src.api.gps_tracks.sources.wikiloc"
save_user_field: false
source_priority: 70
activity_filter: [motorcycle, enduro]
max_tracks_per_run: 50
max_tracks_per_run — soft-cap для первого прогона, чтобы не тратить
часы на rate-limit (см. BRD R-6); реализуется в parser'е через
счётчик внутри collect(). Если поля в parser ещё нет — добавить
поддержку:
max_tracks = self.config.get("max_tracks_per_run")
yielded = 0
# в основном цикле перед yield:
if max_tracks is not None and yielded >= max_tracks:
logger.info("Wikiloc: reached max_tracks_per_run=%d, stopping", max_tracks)
return
yielded += 1
REQ-F-04 — Конфиг: регион tsfo_plus_chuvashia
В config/gps_regions.yaml, запись tsfo_plus_chuvashia.sources
дополняется до [osm, enduro_russia, wikiloc, ttrails]. Порядок
важен: ttrails остаётся, но он enabled: false в sources.yaml — он
автоматически пропускается guard'ом.
Поле enabled: true региона не меняется.
REQ-F-05 — Pipeline licensing-guard
scripts/gps_collect.py:_check_license_adr (строки 37–73) не
изменяется. Перед активацией ET-009 выполнить:
grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md
grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md
Оба значения должны быть accepted. Иначе — STOP и эскалация
архитектору.
REQ-F-06 — Тест-фикстура EnduroRussia API
Создаётся файл tests/fixtures/gps-tracks/enduro-russia-api-tracks-page1.json
с реальным snapshot ответа https://endurorussia.ru/api/tracks?page=0&limit=50.
Минимальные требования к snapshot:
- ≥ 5 items.
- Каждый item содержит
id(int),name(str),difficulty(str),created_at(str ISO). - Поле
total(int) присутствует.
Снимок делается разово, вручную через curl, сохраняется в репо; не зависит от состояния сайта.
REQ-F-07 — Тест-фикстуры EnduroRussia GPX
Создаются 3 файла tests/fixtures/gps-tracks/enduro-russia-track-{1,2,3}.gpx
с реальными GPX-файлами из API. Один из них должен:
- содержать
<trk><trkseg><trkpt>с ≥ 10 точками; - лежать в bbox региона
tsfo_plus_chuvashia(29..47.5 longitude, 49.5..60.0 latitude); - иметь creator или metadata, идентифицирующее источник.
Второй GPX должен быть пустой (<trkseg></trkseg>) или с 0
trkpt — для проверки skip-логики _parse_gpx.
Третий GPX — c одной точкой за пределами bbox — для проверки bbox-фильтрации.
REQ-F-08 — Тест-фикстура Wikiloc HTML страницы поиска
Файл tests/fixtures/gps-tracks/wikiloc-search-page1.html — реальный
снимок GET /wikiloc/find.do?act=19&sw=…&ne=…&page=0. Должен
содержать ≥ 5 ссылок на треки в формате /trails/<slug>/<id>.
REQ-F-09 — Тест-фикстуры Wikiloc страницы трека и GPX
tests/fixtures/gps-tracks/wikiloc-trail-page.html— снимок страницы одного трека Wikiloc; должен содержать<h1>с названием и либо прямую ссылку на.gpx, либоdownloadTrail.do?id=<id>.tests/fixtures/gps-tracks/wikiloc-track.gpx— реальный GPX, возвращаемый/wikiloc/downloadTrail.do?id=<id>для трека, совпадающего по координатам с одним из EnduroRussia-треков — для теста dedup-merge.tests/fixtures/gps-tracks/wikiloc-rate-limited.html— пустой файл (используется в тесте 429, реальный HTML не важен, достаточно тестового мока httpx, который вернёт 429).
REQ-F-10 — Unit-тесты EnduroRussia parser
Файл tests/unit/test_gps_tracks_enduro_russia.py (новый).
Покрытие:
- UT-ER-01.
_parse_gpxпринимает фикстурный GPXenduro-russia-track-1.gpx→ возвращаетTrackInsertсpoints_count >= 10,min_lon/max_lon/min_lat/max_latкорректны,length_m > 0,external_url = "https://endurorussia.ru/tracks/<id>". - UT-ER-02.
_parse_gpxпринимает фикстуруenduro-russia-track-2.gpx(пустой) → возвращаетNone. - UT-ER-03. Bbox-фильтр: трек 3 (точка за пределами региона) при
пересечении с region bbox →
_bbox_intersectsвозвращаетFalse,collect()не yield-ит этот трек. - UT-ER-04.
MAPPINGмаппит"hard" → "enduro","мото" → "moto","unknown" → "other"(default черезmap_activity). - UT-ER-05.
EnduroRussiaParser.__init__принимает конфиг сbase_url: "https://endurorussia.ru"и сохраняет его (без замены на дефис-вариант). Регрессия для R-4. - UT-ER-06.
collect()корректно прерывается, когдаfetched_so_far >= total. - UT-ER-07. При HTTP 429 на
/api/tracks— генератор завершается без exception. - UT-ER-08. При HTTP 429 на
/api/tracks/{id}/gpx— генератор завершается без exception, треки, уже yield-нутые до этого, сохраняются.
REQ-F-11 — Unit-тесты Wikiloc parser
Файл tests/unit/test_gps_tracks_wikiloc.py (новый).
- UT-WL-01.
_extract_track_pathsиз фикстурыwikiloc-search-page1.htmlвозвращает ≥ 5 уникальных путей. - UT-WL-02.
_extract_gpx_url: из HTML сdownloadTrail.do?id=Xвозвращает абсолютный URLhttps://www.wikiloc.com/wikiloc/downloadTrail.do?id=X. - UT-WL-03.
_extract_gpx_url: из HTML без явных ссылок возвращает fallbackhttps://www.wikiloc.com/wikiloc/downloadTrail.do?id=<track_id>. - UT-WL-04.
_extract_track_nameизвлекает текст<h1>. - UT-WL-05.
_parse_gpxна фикстуреwikiloc-track.gpxвозвращаетTrackInsertс правильными bbox иactivity_type='moto'(для activity-категорииmotorcycle). - UT-WL-06.
MAPPINGмаппит"motorcycle" → "moto","hiking" → "hike","mtb" → "bicycle". - UT-WL-07.
collect()останавливается при 403 на странице поиска (graceful-stop). - UT-WL-08.
collect()останавливается при 429 на странице трека, но уже yield-нутые треки сохраняются. - UT-WL-09. Соблюдение
rate_limit_sec: между двумя последовательными HTTP-запросамиasyncio.sleepвызывается с аргументом ≥ конфигурируемого значения. (Mockasyncio.sleep, проверка count и аргументов.) - UT-WL-10.
max_tracks_per_run: приmax_tracks_per_run=2и mock поиске на ≥ 5 треков —collect()yield-ит ровно 2 трека.
REQ-F-12 — Integration-тест pipeline на mock-источниках
Файл tests/integration/test_pipeline_et009.py (новый).
Использует respx или httpx_mock для подмены HTTP. Запускает
scripts/gps_collect.py:main (через asyncio.run) с временной БД.
- IT-ER-01. Pipeline с mock EnduroRussia (фикстурный JSON +
3 GPX) + регион
tsfo_plus_chuvashia→ в БД 2 трека (третий отфильтрован bbox-ом),pipeline_runs[-1].status='ok',tracks_new=2. - IT-WL-01. Pipeline с mock Wikiloc (фикстурный HTML поиска + 1
страница трека + 1 GPX) → в БД 1 трек,
pipeline_runs[-1].status='ok',tracks_new=1. - IT-WL-02. Mock Wikiloc возвращает 403 на странице поиска →
pipeline_runs[-1].status='partial'или'rate_limited',tracks_new=0, exit-code pipeline не 0 (есть error) либо exit-code 0 при условии что graceful-stop не считается error — выбрать одно поведение и зафиксировать тест на нём. Решение: graceful-stop ≠ error, exit-code 0, status'partial'. - IT-DEDUP-01. Pipeline сначала собирает EnduroRussia (1 трек),
затем Wikiloc (1 трек с теми же координатами и длиной ±5%, той же
датой ±1 день) → в БД одна запись с
sources=['enduro_russia','wikiloc'],external_urls=[endurorussia.ru/…, wikiloc.com/…], метаданные имеют приоритетenduro_russia(еслиsource_priority=80выше чем у wikiloc=70 — см. ET-008 dedup-merge). - IT-LIC-01. Искусственно поменять
status: accepted→status: proposedв копии ADR-010 (через временныйGPS_SOURCES_CONFIGenv с другим путём license_adr) → pipeline пропускает source сpipeline_runs[-1].status='skipped_license'.
REQ-F-13 — Стили: цвета по источнику
В файлах src/web/style.json и src/web/style-dark.json слой
gps-tracks-layer (или его эквивалент из ET-008) содержит
match-expression line-color:
[
"match",
["get", "source"],
"osm", "#3cb44b",
"enduro_russia", "#e6194b",
"wikiloc", "#4363d8",
"#808080"
]
Цвета — приближённо, окончательная палитра согласуется с UX в момент реализации. Главное: для всех трёх известных источников ID-→-цвет задан, fallback есть.
Аналогично для gps-tracks-halo-satellite — halo всегда белый/
полупрозрачный, цвет линии берётся тот же.
REQ-F-14 — Атрибуция
После первого прогона, при наличии в БД треков из enduro_russia,
endpoint /api/gps-tracks/health возвращает в поле attributions
(если уже есть в ET-008) или в эквивалентном — список:
["© OpenStreetMap contributors (ODbL)", "EnduroRussia.ru", "© Wikiloc contributors"]
Клиент src/web/gps_tracks.js подмешивает эти строки в MapLibre
attribution control (через map.getControl(...) или эквивалент).
Если в ET-008 атрибуция формируется на клиенте по статическому
маппингу source_id → label — расширить маппинг:
const SOURCE_ATTRIBUTIONS = {
osm: "© OpenStreetMap contributors (ODbL)",
enduro_russia: "EnduroRussia.ru",
wikiloc: "© Wikiloc contributors",
ttrails: "ttrails.ru",
};
REQ-F-15 — Контрактный smoke-тест EnduroRussia API
Файл tests/contract/test_endurorussia_api_smoke.py (новый,
помечается маркером @pytest.mark.network и не запускается в обычном
CI; запускается вручную или в nightly).
- CT-ER-01.
GET https://endurorussia.ru/api/tracks?page=0&limit=5возвращает 200, JSON с ключамиitems,total. - CT-ER-02.
GET https://endurorussia.ru/api/tracks/{first_id}/gpxвозвращает 200, Content-Type содержитxmlилиgpx, тело парситсяdefusedxmlбез exception.
Назначение: при поломке внешнего API мы узнаём об этом из nightly, а не из тишины health-эндпоинта.
REQ-F-16 — Контрактный smoke-тест Wikiloc (опционально)
Из-за rate-limit и риска бана не делаем регулярный smoke-тест
Wikiloc. Вместо этого фиксируем в docs/work-items/ET-009/13-test-report.md
после первой ручной проверки факт того, что find.do отвечает 200 с
ожидаемой структурой.
REQ-F-17 — Первый продакшн-прогон
После мерджа в main и деплоя в test-среду оператор запускает:
ssh mva154
cd /opt/enduro-trails
python scripts/gps_collect.py --region tsfo_plus_chuvashia --source enduro_russia
# (ждать ≈ 25 минут)
python scripts/gps_collect.py --region tsfo_plus_chuvashia --source wikiloc
# (ждать до достижения max_tracks_per_run, обычно 10-20 минут)
Результат фиксируется в 14-deploy-log.md:
tracks_by_source.enduro_russia(ожидаем ≥ 200);tracks_by_source.wikiloc(ожидаем ≥ 1);- длительность каждого прогона;
errors_count(ожидаем 0 или ≤ 5% от tracks_new).
REQ-F-18 — Не менять контракт /api/gps-tracks
Endpoint /api/gps-tracks сохраняет интерфейс ET-008. Новые ID
источников (enduro_russia, wikiloc) появляются в значениях полей
ответа естественным образом; никаких новых query-параметров или
полей в FeatureCollection не вводится.
REQ-F-19 — Не менять алгоритм дедупликации
compute_dedup_key в dedup.py не меняется. Никаких новых правил
для пары (enduro_russia, wikiloc) — стандартный
bbox+length+date-алгоритм должен справиться (см. ADR-006).
REQ-F-20 — Документация
В docs/work-items/ET-009/ должны существовать после Анализа:
00-business-request.md(есть)01-brd.md(создаётся в ET-009)02-trz.md(этот файл)03-acceptance-criteria.md04-test-plan.yaml04b-ui-test-cases.md
После реализации добавляются: 07-infra-requirements.md,
08-data-requirements.md, 10-tech-risks.md, 12-review.md,
13-test-report.md, 14-deploy-log.md.
4. Не-функциональные требования
NFR-01 — Производительность сбора
EnduroRussia: при rate_limit_sec=5 и 305 треках полный прогон
региона tsfo_plus_chuvashia укладывается в ≤ 30 минут (305 × 5
сек ≈ 25 мин + overhead).
Wikiloc: первый прогон ограничен max_tracks_per_run=50 →
максимум 50 × (1 search + 1 trail + 1 gpx) × 10 сек ≈ 25 минут.
NFR-02 — Стабильность
Падение Wikiloc-парсера не должно валить весь pipeline. Покрывается
существующей логикой scripts/gps_collect.py (per-source error
не помечает остальные как error).
NFR-03 — Размер БД
Прирост data/gps_tracks.sqlite после первого прогона ET-009:
≤ 100 MB при 200 треков EnduroRussia + 50 Wikiloc. Если фактический
прирост существенно больше — фиксируется в 14-deploy-log.md.
NFR-04 — Логирование
Pipeline и parser используют существующий logger стандартного
формата. Никаких новых форматов или sinks ET-009 не добавляет.
NFR-05 — Безопасность
XML-парсинг GPX выполняется через defusedxml.ElementTree (как в
ET-008). Никаких изменений по security ET-009 не вносит.
NFR-06 — Совместимость
Контракт /api/gps-tracks* не меняется. Существующие клиенты
(включая старые версии браузеров пользователей) продолжают работать
без обновления.
5. План работ (для разработчика)
- Сверка ADR-010 / ADR-012 →
status: accepted(REQ-F-05). Если нет — STOP. - Правка
config/gps_sources.yaml(REQ-F-01, F-02, F-03). - Правка
config/gps_regions.yaml(REQ-F-04). - Снапшот реальных ответов API/HTML и сохранение как фикстуры (REQ-F-06..F-09). Снимки берутся до unit-тестов, чтобы тесты опирались на реальные данные.
- Расширение Wikiloc-парсера
max_tracks_per_run(если ещё нет). - Написание unit-тестов (REQ-F-10, F-11).
- Написание integration-тестов (REQ-F-12).
- Контрактный smoke-тест EnduroRussia (REQ-F-15).
- Расширение стилей карты (REQ-F-13).
- Атрибуция в клиенте (REQ-F-14).
- Прогон всех тестов локально (
make test). - Code review → merge → deploy в test.
- Ручной первый прогон (REQ-F-17). Запись в
14-deploy-log.md. - Проверка UI по тест-плану
04b-ui-test-cases.md.
6. Открытые вопросы и решения по умолчанию
| Вопрос | Решение по умолчанию |
|---|---|
| Считать ли graceful-stop Wikiloc ошибкой? | Нет. pipeline_runs.status='partial', exit-code 0. (См. IT-WL-02.) |
| Запускать ли cron автоматически после ET-009? | Нет. Cron включается отдельным DevOps-task'ом после двух успешных ручных прогонов подряд. |
Маппить ли wikiloc.act=motorcycle (19) на enduro или moto? |
moto (более широкая категория). MAPPING уже так сконфигурирован. |
Что делать с старым URL enduro-russia.ru в external_url ранее собранных треков? |
Опциональный one-shot UPDATE-скрипт; в ET-009 не обязателен (база test-среды чистая для практических целей). |
Wikiloc возвращает creator=Wikiloc в GPX тех же треков, что и EnduroRussia? |
Нормально — на это и нужен dedup-merge. |
| Нужно ли менять source_priority? | Нет. osm=100, enduro_russia=80, wikiloc=70 — порядок задаёт приоритет метаданных при merge. |