Files
Sadmin d5a270f3af B22: прикладное логирование vector.* + администрирование в доках (по образцу РВС)
- backend/logging_setup.py: setup_logging() (stdout INFO + файл /tmp/vector/vector.log,
  ротация 5×2 МБ, VectorLOGLEVEL/VectorLOGFILE/VectorLOGDIR), get_logger('vector.*')
- события: vector.analyze (матрасчёт с длительностью), vector.auth (login_ok/failed/
  lockout), vector.audit (каждая запись), vector.contour (ok/failed/rejected),
  vector.operations (operation_complete), vector.geocode (успех/timeout/ошибка)
- существующие логгеры переведены в namespace vector.* (geo, geo.osm, rules, water, admin)
- README §13а: таблица наблюдаемых событий + правила (без ПДн/секретов)
- SETUP_GUIDE: §15 RPO/RTO + backup≠restore, §16 grep по vector.*, §17 диагностика
  по цепочкам (БД, 500, fallback-зоны, вход), §17а чек-лист передачи проекта
- DEPLOY.md: вместо устаревшего дубля — ссылка на актуальные доки + шпаргалка стенда
- тесты: 239 passed
2026-09-25 09:38:39 +03:00

82 lines
2.8 KiB
Python

"""Геокодирование: прокси к Nominatim (OpenStreetMap).
GET /api/v1/geocode?q=<запрос>&limit=5
Возвращает {results: [{lat, lng, display_name, type}]}.
Nominatim Usage Policy: User-Agent обязателен, ≤1 rps — проксируем через
бэкенд с корректным UA и ограничением countrycodes=by.
Правила запросов (nominatim-грабли, см. homelab-ops skill):
- Короткий запрос: пункт + адрес, БЕЗ района/области.
- Пункт ПЕРВЫМ словом.
"""
import os
import logging
from typing import List, Optional
import httpx
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from backend.logging_setup import get_logger
from backend.routers.auth import require_roles
from fastapi import Depends
log = get_logger('geocode')
router = APIRouter(prefix='/api/v1/geocode', tags=['geocode'])
NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
USER_AGENT = os.getenv('NOMINATIM_USER_AGENT', 'Vector-MChS/1.0 (geocoding proxy)')
COUNTRY_CODE = 'by'
class GeocodeResult(BaseModel):
lat: float
lng: float
display_name: str
type: Optional[str] = None
class GeocodeResponse(BaseModel):
results: List[GeocodeResult]
@router.get('', response_model=GeocodeResponse,
dependencies=[Depends(require_roles(['operator', 'field', 'admin']))])
async def geocode(
q: str = Query(..., min_length=2, description='Адрес: «пункт, улица»'),
limit: int = Query(5, ge=1, le=10),
):
"""Геокодирование через Nominatim, ограничено Беларусью."""
headers = {'User-Agent': USER_AGENT}
params = {
'q': q.strip(),
'format': 'jsonv2',
'limit': limit,
'countrycodes': COUNTRY_CODE,
'addressdetails': 1,
}
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(NOMINATIM_URL, headers=headers, params=params)
resp.raise_for_status()
data = resp.json()
except httpx.TimeoutException:
log.warning('geocode timeout q=%s', q.strip()[:100])
raise HTTPException(status_code=504, detail='Сервис геокодирования не ответил')
except Exception as e:
log.warning('geocode error q=%s: %s', q.strip()[:100], e)
raise HTTPException(status_code=502, detail=f'Ошибка геокодирования: {e}')
log.info('geocode q=%s результатов=%d', q.strip()[:100], len(data))
return {'results': [
GeocodeResult(
lat=float(item['lat']),
lng=float(item['lon']),
display_name=item.get('display_name', ''),
type=item.get('type'),
)
for item in data
]}