Backend:
- Миграция gps_tracks_001_init.sql: таблицы tracks + pipeline_runs
- Пакет src/api/gps_tracks/: models, db (WAL+upsert с dedup), dedup
(bbox+length+date bucket-hash), mvt (LRU-кэш 1024 тайла), endpoint
(GET /api/gps-tracks, GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt,
GET /api/gps-tracks/health, POST /api/gps-tracks/cache/clear), config
- Парсеры: osm (split_bbox, haversine, defusedxml XXE-защита),
enduro_russia + ttrails — заглушки (ADR-010/011 proposed, блокированы)
- Licensing guard: pipeline проверяет status ADR-файла до запуска источника
- scripts/gps_collect.py: CLI с --region/--source/--dry-run/--gc
Frontend:
- src/web/gps_tracks.js: двухрежимный слой (MVT z≤11, GeoJSON z≥12),
debounced fetch + AbortController, фильтры активности/источника,
цветовая палитра by-source/by-activity, halo на спутнике, popup трека,
restorePublicTracksState(), localStorage persistence
- index.html: чекбокс «Публичные треки» в terrain-popup, #sheet-gps-filters
- app.css: .terrain-link-btn, .gps-filter-grid, .track-popup
- app.js: вызов restorePublicTracksState() в rebuildMapOverlays(),
applyGpsHaloVisibility() в applyBaseLayer()
Конфиги:
- config/gps_sources.yaml: osm (enabled), enduro_russia/ttrails (disabled)
- config/gps_regions.yaml: ЦФО+Чувашия (enabled), Кавказ (disabled)
Docker:
- gps-collector service с profiles: [batch]
Тесты: 48 новых тестов (unit + integration), 125/125 pass
Refs: ET-008
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
3.1 KiB
Python
90 lines
3.1 KiB
Python
"""Загрузка и валидация конфигурации GPS-источников и регионов (ET-008)."""
|
||
import yaml
|
||
|
||
|
||
def load_sources_config(path: str) -> list:
|
||
"""Загружает конфигурацию источников GPS-треков.
|
||
|
||
Args:
|
||
path: путь к YAML-файлу конфигурации источников
|
||
|
||
Returns:
|
||
list[dict] — список источников
|
||
|
||
Raises:
|
||
ValueError: при ошибках валидации
|
||
FileNotFoundError: если файл не найден
|
||
"""
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
data = yaml.safe_load(f)
|
||
|
||
sources = data.get("sources", [])
|
||
if not isinstance(sources, list):
|
||
raise ValueError("sources must be a list")
|
||
|
||
for src in sources:
|
||
if not src.get("id"):
|
||
raise ValueError(f"Source missing 'id': {src}")
|
||
if not src.get("base_url"):
|
||
raise ValueError(f"Source '{src['id']}' missing 'base_url'")
|
||
|
||
# Enabled source must have license_adr
|
||
if src.get("enabled", False):
|
||
if not src.get("license_adr"):
|
||
raise ValueError(
|
||
f"Enabled source '{src['id']}' must have 'license_adr'"
|
||
)
|
||
|
||
return sources
|
||
|
||
|
||
def load_regions_config(path: str) -> list:
|
||
"""Загружает конфигурацию регионов для сбора GPS-треков.
|
||
|
||
Args:
|
||
path: путь к YAML-файлу конфигурации регионов
|
||
|
||
Returns:
|
||
list[dict] — список регионов
|
||
|
||
Raises:
|
||
ValueError: при ошибках валидации
|
||
FileNotFoundError: если файл не найден
|
||
"""
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
data = yaml.safe_load(f)
|
||
|
||
regions = data.get("regions", [])
|
||
if not isinstance(regions, list):
|
||
raise ValueError("regions must be a list")
|
||
|
||
for reg in regions:
|
||
if not reg.get("id"):
|
||
raise ValueError(f"Region missing 'id': {reg}")
|
||
|
||
bbox = reg.get("bbox")
|
||
if not bbox or len(bbox) != 4:
|
||
raise ValueError(f"Region '{reg['id']}' must have bbox with 4 values")
|
||
|
||
west, south, east, north = bbox
|
||
|
||
# Валидация диапазонов координат
|
||
if not (-180 <= west <= 180):
|
||
raise ValueError(f"Region '{reg['id']}' bbox west={west} out of range")
|
||
if not (-180 <= east <= 180):
|
||
raise ValueError(f"Region '{reg['id']}' bbox east={east} out of range")
|
||
if not (-90 <= south <= 90):
|
||
raise ValueError(f"Region '{reg['id']}' bbox south={south} out of range")
|
||
if not (-90 <= north <= 90):
|
||
raise ValueError(f"Region '{reg['id']}' bbox north={north} out of range")
|
||
if west >= east:
|
||
raise ValueError(
|
||
f"Region '{reg['id']}' bbox: west must be < east"
|
||
)
|
||
if south >= north:
|
||
raise ValueError(
|
||
f"Region '{reg['id']}' bbox: south must be < north"
|
||
)
|
||
|
||
return regions
|