auto-sync: 2026-06-01 20:30:01
This commit is contained in:
430
tasks/enduro-trails/DEV_TASK_ET009_NEW_SOURCES.md
Normal file
430
tasks/enduro-trails/DEV_TASK_ET009_NEW_SOURCES.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# 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-агент*
|
||||
Reference in New Issue
Block a user