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

431 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: `SourceParser``gps_collect.py``gps_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:
```yaml
status: accepted
```
Добавить в тело ADR раздел "## Решение (финальное)":
```markdown
## Решение (финальное — 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 может меняться → парсер хрупкий
```yaml
---
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+)
Логика парсера:
```python
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:
```python
# Из <trkpt lat="..." lon="..."> собрать список (lon, lat) точек
# length_m = сумма haversine расстояний между точками
# WKB = LineString из shapely или ручная упаковка (как в osm.py)
# bbox = min/max lon/lat
```
- [ ] **3.2** Проверить что парсер работает на одном треке:
```bash
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`
Парсинг:
```python
# httpx GET страницы поиска
# Найти ссылки на треки: <a href="/wikiloc/view.do?id=...">
# Для каждого трека: GET страницы трека
# Найти ссылку на GPX: <a href="...download...gpx...">
# Скачать GPX → распарсить геометрию
```
- [ ] **4.2** Реализовать `WikilockParser.collect()`:
```python
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** Проверить:
```bash
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`:
```yaml
- 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:
```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", "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 запросов):
```python
# Фикстура: минимальный GPX XML для EnduroRussia
# Фикстура: минимальный HTML страницы Wikiloc с треком
# Тест: EnduroRussiaParser парсит GPX → TrackInsert с корректными полями
# Тест: WikilockParser парсит HTML → находит ссылки на треки
# Тест: map_activity маппит категории правильно
```
- [ ] **6.2** Запустить тесты:
```bash
cd /home/slin/repos/enduro-trails
python3 -m pytest tests/test_sources_et009.py -v
```
**Критерий готовности:** все тесты зелёные
---
### Task 7: Деплой и smoke-тест
**Шаги:**
- [ ] **7.1** Собрать и запустить коллектор:
```bash
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, ограниченный регион):
```bash
docker compose run --rm gps-collector python -m scripts.gps_collect --source enduro_russia --region north_caucasus
```
- [ ] **7.3** Проверить БД:
```bash
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 (Москва):
```bash
docker compose run --rm gps-collector python -m scripts.gps_collect --source wikiloc --region moscow_test
```
Если 403/429 — зафиксировать в логах, это ожидаемо. Главное — graceful handling без краша.
- [ ] **7.5** Закоммитить:
```bash
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-агент*