fix(sf): робастный путь к koltsovo JSON в pat-loader (#1150) #1155
2 changed files with 100 additions and 7 deletions
|
|
@ -20,6 +20,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -52,9 +53,38 @@ _KOLTSOVO_LON_MAX = 61.2
|
|||
_KOLTSOVO_LAT_MIN = 56.5
|
||||
_KOLTSOVO_LAT_MAX = 57.0
|
||||
|
||||
# Путь к JSON относительно repo root (backend/../data/pat/…)
|
||||
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[4]
|
||||
_JSON_PATH = _REPO_ROOT / "data" / "pat" / "koltsovo_367p_subzones.json"
|
||||
# Путь к JSON — робастный резолв: контейнер (/app/data/...) и dev-среда (repo/data/...)
|
||||
# В контейнере: WORKDIR=/app, код в /app/app/..., данные в /app/data/...
|
||||
# parents[3] от /app/app/workers/tasks/ = /app → /app/data/pat/ ✓
|
||||
# В dev: код в <repo>/backend/app/workers/tasks/, данные в <repo>/data/...
|
||||
# parents[4] от <repo>/backend/app/workers/tasks/ = <repo> → <repo>/data/pat/ ✓
|
||||
# Опция: PAT_JSON_PATH env-переменная переопределяет всё.
|
||||
_PAT_JSON_RELATIVE = pathlib.Path("data") / "pat" / "koltsovo_367p_subzones.json"
|
||||
_THIS = pathlib.Path(__file__).resolve()
|
||||
_JSON_PATH_CANDIDATES: list[pathlib.Path] = []
|
||||
|
||||
if _env_path := os.environ.get("PAT_JSON_PATH"):
|
||||
_JSON_PATH_CANDIDATES.append(pathlib.Path(_env_path))
|
||||
# parents[3] — контейнерный корень /app
|
||||
_JSON_PATH_CANDIDATES.append(_THIS.parents[3] / _PAT_JSON_RELATIVE)
|
||||
# parents[4] — repo root в dev
|
||||
_JSON_PATH_CANDIDATES.append(_THIS.parents[4] / _PAT_JSON_RELATIVE)
|
||||
|
||||
|
||||
def _resolve_json_path() -> pathlib.Path:
|
||||
"""Возвращает первый существующий кандидат JSON, иначе первый (для понятного сообщения)."""
|
||||
for candidate in _JSON_PATH_CANDIDATES:
|
||||
if candidate.exists():
|
||||
logger.debug("pat_subzones_load: JSON путь → %s", candidate)
|
||||
return candidate
|
||||
# Файл не найден ни по одному кандидату — вернём первый (контейнерный) для диагностики
|
||||
primary = _JSON_PATH_CANDIDATES[0] if _JSON_PATH_CANDIDATES else _THIS / "MISSING"
|
||||
logger.warning(
|
||||
"pat_subzones_load: JSON не найден ни по одному кандидату: %s",
|
||||
[str(c) for c in _JSON_PATH_CANDIDATES],
|
||||
)
|
||||
return primary
|
||||
|
||||
|
||||
_SOURCE = "Приказ Росавиации № 367-П от 02.06.2023 (ред. Приказа Росавиации от 27.10.2025 № 804-П)"
|
||||
|
||||
|
|
@ -188,12 +218,14 @@ def load_koltsovo_pat() -> dict[str, Any]:
|
|||
"""
|
||||
from app.core.db import SessionLocal
|
||||
|
||||
logger.info("pat_subzones_load: start, json=%s", _JSON_PATH)
|
||||
json_path = _resolve_json_path()
|
||||
logger.info("pat_subzones_load: start, json=%s", json_path)
|
||||
|
||||
if not _JSON_PATH.exists():
|
||||
raise FileNotFoundError(f"JSON не найден: {_JSON_PATH}")
|
||||
if not json_path.exists():
|
||||
candidates_str = ", ".join(str(c) for c in _JSON_PATH_CANDIDATES)
|
||||
raise FileNotFoundError(f"JSON не найден: {json_path}. Кандидаты: {candidates_str}")
|
||||
|
||||
with open(_JSON_PATH, encoding="utf-8") as fh:
|
||||
with open(json_path, encoding="utf-8") as fh:
|
||||
data: dict[str, Any] = json.load(fh)
|
||||
|
||||
aerodrome: str = data.get("aerodrome", "Екатеринбург (Кольцово)")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import pathlib
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
|
@ -266,3 +268,62 @@ def test_pat_lookup_graceful_programming_error() -> None:
|
|||
db = _FakeDB(raise_exc=ProgrammingError("stmt", {}, Exception("column not found")))
|
||||
result = parcel_pat_subzones(db, _WKT)
|
||||
assert result == []
|
||||
|
||||
|
||||
# ── Резолв пути к JSON (#1150) ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_json_path_candidates_non_empty() -> None:
|
||||
"""_JSON_PATH_CANDIDATES содержит хотя бы один кандидат."""
|
||||
from app.workers.tasks.pat_subzones_load import _JSON_PATH_CANDIDATES
|
||||
|
||||
assert len(_JSON_PATH_CANDIDATES) >= 1
|
||||
|
||||
|
||||
def test_json_path_candidates_end_with_expected_suffix() -> None:
|
||||
"""Каждый кандидат заканчивается на data/pat/koltsovo_367p_subzones.json."""
|
||||
import pathlib
|
||||
|
||||
from app.workers.tasks.pat_subzones_load import _JSON_PATH_CANDIDATES
|
||||
|
||||
expected_suffix = pathlib.Path("data") / "pat" / "koltsovo_367p_subzones.json"
|
||||
for candidate in _JSON_PATH_CANDIDATES:
|
||||
# последние 3 части пути совпадают с ожидаемыми
|
||||
assert (
|
||||
candidate.parts[-3:] == expected_suffix.parts
|
||||
), f"Кандидат {candidate} не заканчивается на {expected_suffix}"
|
||||
|
||||
|
||||
def test_resolve_json_path_finds_real_file() -> None:
|
||||
"""_resolve_json_path() находит JSON-файл в репо (data/pat/ на уровне repo root)."""
|
||||
from app.workers.tasks.pat_subzones_load import _resolve_json_path
|
||||
|
||||
resolved = _resolve_json_path()
|
||||
assert resolved.exists(), (
|
||||
f"JSON-файл подзон ПАТ не найден по пути {resolved}. "
|
||||
"Убедитесь что data/pat/koltsovo_367p_subzones.json присутствует в репо."
|
||||
)
|
||||
assert resolved.name == "koltsovo_367p_subzones.json"
|
||||
|
||||
|
||||
def test_resolve_json_path_env_override(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
||||
) -> None:
|
||||
"""PAT_JSON_PATH env-переменная переопределяет путь к JSON."""
|
||||
import app.workers.tasks.pat_subzones_load as mod
|
||||
|
||||
# Создаём временный файл
|
||||
fake_json = tmp_path / "fake_subzones.json"
|
||||
fake_json.write_text("{}", encoding="utf-8")
|
||||
|
||||
monkeypatch.setenv("PAT_JSON_PATH", str(fake_json))
|
||||
|
||||
# Перезагружаем модуль чтобы env-переменная была подхвачена на уровне модуля
|
||||
mod_reloaded = importlib.reload(mod)
|
||||
|
||||
resolved = mod_reloaded._resolve_json_path()
|
||||
assert resolved == fake_json
|
||||
|
||||
# Откат — перезагрузить без env
|
||||
monkeypatch.delenv("PAT_JSON_PATH", raising=False)
|
||||
importlib.reload(mod)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue