Files
wiki/tasks/enduro-trails/DEV_TASK_ET009_NEW_SOURCES.md
2026-06-01 20:30:01 +03:00

18 KiB
Raw Blame History

DEV TASK: ET-009 — Новые источники GPS-треков: EnduroRussia + Wikiloc

Статус: Ready for dev
Проект: enduro-trails
Фаза: ET-009
BRD: docs/work-items/ET-008/01-brd.md (расширение §4)


Цель

Реализовать два новых парсера GPS-треков — EnduroRussia.ru (JSON API) и Wikiloc (HTML-парсинг) — и активировать их в pipeline, обновив ADR-010 и создав ADR-012 для Wikiloc.


Архитектура

Оба источника встраиваются в существующую архитектуру ET-008: SourceParsergps_collect.pygps_tracks.sqlite. EnduroRussia имеет открытый JSON API (/api/tracks + /api/tracks/{id}/gpx) — парсер простой и надёжный. Wikiloc не имеет публичного API, поэтому используем HTTP-парсинг страниц поиска с httpx + lxml/html.parser, с агрессивным rate limiting и User-Agent.


Стек / Зависимости

  • Python 3.11, asyncio, httpx (уже в проекте)
  • defusedxml (уже в проекте) — для парсинга GPX от EnduroRussia
  • lxml или html.parser (stdlib) — для Wikiloc HTML
  • gpxpy или ручной парсинг GPX — для извлечения геометрии
  • Без новых pip-зависимостей если возможно (использовать stdlib xml + httpx)

Инфраструктура

Параметр Значение
Сервер slin@82.22.50.71
Рабочая директория /home/slin/repos/enduro-trails/
Деплой docker compose build && docker compose run --rm gps-collector
Проверка БД sqlite3 /home/slin/repos/enduro-trails/data/gps_tracks.sqlite "SELECT source_id, count(*) FROM tracks GROUP BY source_id;"

Файловая карта

Действие Файл Ответственность
Изменить src/api/gps_tracks/sources/enduro_russia.py Реализовать парсер (сейчас заглушка)
Создать src/api/gps_tracks/sources/wikiloc.py Новый парсер Wikiloc
Изменить config/gps_sources.yaml Включить enduro_russia, добавить wikiloc
Изменить docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md status: proposed → accepted
Создать docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md Новый ADR для Wikiloc
Изменить tests/test_sources.py или создать Тесты для обоих парсеров

Задачи

Task 1: ADR-010 — разблокировать EnduroRussia

Файлы:

  • Изменить: docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md

Шаги:

  • 1.1 Обновить ADR-010: заполнить §1-§3 на основе реального API

Факты для заполнения:

  • API публичный, без авторизации: GET https://endurorussia.ru/api/tracks → JSON
  • GPX скачивается напрямую: GET https://endurorussia.ru/api/tracks/{id}/gpx
  • robots.txt проверить: curl https://endurorussia.ru/robots.txt
  • ToS: проверить https://endurorussia.ru/about или footer
  • Треки содержат creator="Wikiloc" в GPX — источник данных Wikiloc, агрегатор
  • Attribution: "EnduroRussia.ru"

Изменить в frontmatter:

status: accepted

Добавить в тело ADR раздел "## Решение (финальное)":

## Решение (финальное — accepted)

API публичный и открытый (без авторизации, без ключей).
robots.txt: [результат проверки]
ToS: [результат проверки]
Треки содержат creator="Wikiloc" — платформа агрегирует публичные треки.
Attribution в pipeline: "EnduroRussia.ru"
status → accepted [дата]

Критерий готовности: grep "status: accepted" ADR-010-enduro-russia-licensing.md → найдено


Task 2: ADR-012 — создать для Wikiloc

Файлы:

  • Создать: docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md

Шаги:

  • 2.1 Создать ADR-012 по образцу ADR-010, со статусом accepted

Факты:

  • Wikiloc не имеет публичного API (официально отказали)
  • Треки публичны на сайте, доступны без авторизации через HTML
  • Парсинг HTML страниц поиска — единственный вариант
  • Rate limit: 10 сек между запросами, User-Agent с контактом
  • Attribution: "© Wikiloc contributors"
  • Риск: структура HTML может меняться → парсер хрупкий
---
type: adr
work_item_id: ET-009
adr_id: ADR-012
title: "ADR-012: Источник Wikiloc — HTML-парсинг публичных треков"
status: accepted
created_at: 2026-06-01
authors:
  - "agent:developer"
---

Критерий готовности: файл создан, status: accepted


Task 3: Парсер EnduroRussia

Файлы:

  • Изменить: src/api/gps_tracks/sources/enduro_russia.py

Шаги:

  • 3.1 Реализовать EnduroRussiaParser.collect()

API эндпоинты (проверено вручную):

  • GET https://endurorussia.ru/api/tracks{"items": [{id, name, description, region, difficulty, length_km, elevation_gain, created_at, user: {id, username}}, ...]}
  • GET https://endurorussia.ru/api/tracks/{id}/gpx → GPX XML (application/gpx+xml)
  • Пагинация: ?page=N&limit=50 (проверить наличие поля total или next)
  • Всего треков: ~305 (id от 1 до 305+)

Логика парсера:

class EnduroRussiaParser(SourceParser):
    MAPPING = {
        "enduro": "enduro",
        "мото": "moto",
        "hard": "enduro",
        "soft": "enduro",
        "тур": "moto",
    }

    async def collect(self, bbox, ctx):
        # 1. GET /api/tracks?page=0&limit=50 — получить список всех треков
        # 2. Для каждого трека: GET /api/tracks/{id}/gpx — скачать GPX
        # 3. Распарсить GPX → извлечь точки трека → WKB геометрию
        # 4. Проверить что bbox трека пересекается с запрошенным bbox
        # 5. Yield TrackInsert(...)
        # Rate limit: 5 сек между запросами (из конфига)
        # external_id: str(track_id)
        # external_url: f"https://endurorussia.ru/tracks/{track_id}"
        # source_id: "enduro_russia"
        # activity_type: map_activity(difficulty или "enduro")

GPX парсинг — использовать уже существующий паттерн из osm.py с defusedxml.

Извлечение геометрии из GPX:

# Из <trkpt lat="..." lon="..."> собрать список (lon, lat) точек
# length_m = сумма haversine расстояний между точками
# WKB = LineString из shapely или ручная упаковка (как в osm.py)
# bbox = min/max lon/lat
  • 3.2 Проверить что парсер работает на одном треке:
cd /home/slin/repos/enduro-trails
python3 -c "
import asyncio
from src.api.gps_tracks.sources.enduro_russia import EnduroRussiaParser
cfg = {'id': 'enduro_russia', 'base_url': 'https://endurorussia.ru', 'rate_limit_sec': 2}
p = EnduroRussiaParser(cfg)
async def test():
    count = 0
    async for t in p.collect((29.0, 49.5, 47.5, 60.0), {}):
        print(t.name, t.length_m, t.points_count)
        count += 1
        if count >= 3:
            break
asyncio.run(test())
"

Ожидаемый результат: 3 трека с name, length_m > 0, points_count > 0

Критерий готовности: парсер возвращает треки без ошибок


Task 4: Парсер Wikiloc

Файлы:

  • Создать: src/api/gps_tracks/sources/wikiloc.py

Шаги:

  • 4.1 Исследовать структуру HTML Wikiloc для поиска треков по bbox

URL поиска: https://www.wikiloc.com/wikiloc/find.do?act=19&sw=49.5,29.0&ne=60.0,47.5&page=0

  • act=19 — мотоциклы/эндуро (проверить актуальные коды активностей)
  • sw=lat,lon — юго-западный угол bbox
  • ne=lat,lon — северо-восточный угол bbox

Альтернативный URL: https://www.wikiloc.com/trails/motorcycle/russia

Парсинг:

# httpx GET страницы поиска
# Найти ссылки на треки: <a href="/wikiloc/view.do?id=...">
# Для каждого трека: GET страницы трека
# Найти ссылку на GPX: <a href="...download...gpx...">
# Скачать GPX → распарсить геометрию
  • 4.2 Реализовать WikilockParser.collect():
class WikilockParser(SourceParser):
    MAPPING = {
        "motorcycle": "moto",
        "enduro": "enduro", 
        "mtb": "bicycle",
        "hiking": "hike",
        "running": "hike",
    }

    async def collect(self, bbox, ctx):
        # 1. Поиск треков по bbox через страницу поиска
        # 2. Для каждого трека: скачать GPX
        # 3. Распарсить → TrackInsert
        # Rate limit: 10 сек (жёстко, Wikiloc агрессивно блокирует)
        # User-Agent: из конфига (с контактом)
        # external_id: wikiloc track id из URL
        # external_url: https://www.wikiloc.com/wikiloc/view.do?id={id}
        # При 429/403: логировать и прекращать (не ретраить агрессивно)
  • 4.3 Проверить:
cd /home/slin/repos/enduro-trails
python3 -c "
import asyncio
from src.api.gps_tracks.sources.wikiloc import WikilockParser
cfg = {'id': 'wikiloc', 'base_url': 'https://www.wikiloc.com', 'rate_limit_sec': 10}
p = WikilockParser(cfg)
async def test():
    count = 0
    async for t in p.collect((37.0, 55.0, 40.0, 57.0), {}):
        print(t.name, t.length_m, t.points_count)
        count += 1
        if count >= 2:
            break
asyncio.run(test())
"

Критерий готовности: парсер возвращает хотя бы 1 трек без ошибок (или graceful skip при 403)


Task 5: Обновить конфиг и включить источники

Файлы:

  • Изменить: config/gps_sources.yaml

Шаги:

  • 5.1 Обновить enduro_russia entry: enabled: true, исправить base_url:
- id: enduro_russia
  name: "EnduroRussia.ru"
  enabled: true
  license_adr: "docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md"
  base_url: "https://endurorussia.ru"
  rate_limit_sec: 5
  user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
  attribution: "EnduroRussia.ru"
  parser_module: "src.api.gps_tracks.sources.enduro_russia"
  save_user_field: false
  source_priority: 80
  • 5.2 Добавить wikiloc entry:
- 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", "mtb"]

Критерий готовности: python3 -c "from src.api.gps_tracks.config import load_sources_config; print([s['id'] for s in load_sources_config('config/gps_sources.yaml').sources])" → содержит enduro_russia и wikiloc


Task 6: Тесты

Файлы:

  • Создать/изменить: tests/test_sources_et009.py

Шаги:

  • 6.1 Написать unit-тесты с фикстурами (без реальных HTTP запросов):
# Фикстура: минимальный GPX XML для EnduroRussia
# Фикстура: минимальный HTML страницы Wikiloc с треком
# Тест: EnduroRussiaParser парсит GPX → TrackInsert с корректными полями
# Тест: WikilockParser парсит HTML → находит ссылки на треки
# Тест: map_activity маппит категории правильно
  • 6.2 Запустить тесты:
cd /home/slin/repos/enduro-trails
python3 -m pytest tests/test_sources_et009.py -v

Критерий готовности: все тесты зелёные


Task 7: Деплой и smoke-тест

Шаги:

  • 7.1 Собрать и запустить коллектор:
cd /home/slin/repos/enduro-trails
docker compose build gps-collector
docker compose run --rm gps-collector python -m scripts.gps_collect --source enduro_russia --dry-run
  • 7.2 Если dry-run OK — запустить реальный сбор (только EnduroRussia, ограниченный регион):
docker compose run --rm gps-collector python -m scripts.gps_collect --source enduro_russia --region north_caucasus
  • 7.3 Проверить БД:
sqlite3 /home/slin/repos/enduro-trails/data/gps_tracks.sqlite \
  "SELECT source_id, count(*), round(avg(length_m)/1000,1) as avg_km FROM tracks GROUP BY source_id;"

Ожидаемый результат: строка enduro_russia | N | X.X где N > 0

  • 7.4 Wikiloc — запустить с ограниченным bbox (Москва):
docker compose run --rm gps-collector python -m scripts.gps_collect --source wikiloc --region moscow_test

Если 403/429 — зафиксировать в логах, это ожидаемо. Главное — graceful handling без краша.

  • 7.5 Закоммитить:
cd /home/slin/repos/enduro-trails
git add -A
git commit -m "feat(ET-009): add EnduroRussia and Wikiloc GPS sources"
git push

Критерий готовности: коммит запушен, в БД есть треки от enduro_russia


Проверка (Acceptance)

# Проверка Команда Ожидаемый результат
1 ADR-010 accepted grep "status: accepted" docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md найдено
2 ADR-012 создан cat docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md | grep status status: accepted
3 Парсер EnduroRussia работает python3 -c "..." (Task 3.2) 3 трека без ошибок
4 Конфиг обновлён grep "enabled: true" config/gps_sources.yaml | wc -l ≥ 2
5 Треки в БД sqlite3 data/gps_tracks.sqlite "SELECT source_id, count(*) FROM tracks GROUP BY source_id;" строка enduro_russia с N > 0
6 Тесты зелёные python3 -m pytest tests/test_sources_et009.py -v все PASSED
7 Коммит запушен git log --oneline -1 feat(ET-009): ...

Ограничения и контекст

  • ⚠️ ADR licensing guard: pipeline проверяет status в ADR файле перед запуском source. Пока status: proposed — source пропускается. Нужно обновить ADR ДО включения enabled: true в конфиге.
  • ⚠️ base_url в конфиге: текущий конфиг содержит https://enduro-russia.ru (с дефисом), реальный URL https://endurorussia.ru (без дефиса). Исправить.
  • ⚠️ Wikiloc без API: парсинг HTML хрупкий. Если структура изменилась — парсер вернёт 0 треков, не упадёт. Логировать предупреждение.
  • ⚠️ Wikiloc rate limit: 10 сек минимум. При 429 — остановить сбор, записать в pipeline_runs.status = "rate_limited", не ретраить.
  • ⚠️ GPX от EnduroRussia содержит Wikiloc-треки: дедупликация по dedup_key (bbox-bucket + length + date) должна отловить дубли если один трек есть и в Wikiloc и в EnduroRussia.
  • ⚠️ docker compose run для gps-collector использует profile batch — нужно --profile batch или run --rm gps-collector.
  • 🚫 Не трогать osm.py и существующие тесты ET-008.
  • 🚫 Не менять схему БД — только новые source_id в существующих таблицах.
  • 🚫 Не добавлять новые pip-зависимости без крайней необходимости (использовать stdlib + уже установленные).

Деплой-чеклист

  • ADR-010 обновлён (status: accepted)
  • ADR-012 создан (status: accepted)
  • enduro_russia.py реализован (не заглушка)
  • wikiloc.py создан
  • gps_sources.yaml обновлён (оба enabled: true)
  • Тесты написаны и зелёные
  • docker compose build прошёл без ошибок
  • Dry-run EnduroRussia — OK
  • Реальный сбор EnduroRussia — треки в БД
  • Wikiloc — graceful handling (треки или 403 без краша)
  • Коммит запушен в main

Создано: 2026-06-01 | Автор ТЗ: Стрим | Исполнитель: Dev-агент