fix(sf): робастный путь к koltsovo JSON в pat-loader (#1155)
Co-authored-by: bot-backend <bot-backend@gendsgn.local> Co-committed-by: bot-backend <bot-backend@gendsgn.local>
This commit is contained in:
parent
08acc25c43
commit
515ac89eaa
2 changed files with 100 additions and 7 deletions
|
|
@ -20,6 +20,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -52,9 +53,38 @@ _KOLTSOVO_LON_MAX = 61.2
|
||||||
_KOLTSOVO_LAT_MIN = 56.5
|
_KOLTSOVO_LAT_MIN = 56.5
|
||||||
_KOLTSOVO_LAT_MAX = 57.0
|
_KOLTSOVO_LAT_MAX = 57.0
|
||||||
|
|
||||||
# Путь к JSON относительно repo root (backend/../data/pat/…)
|
# Путь к JSON — робастный резолв: контейнер (/app/data/...) и dev-среда (repo/data/...)
|
||||||
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[4]
|
# В контейнере: WORKDIR=/app, код в /app/app/..., данные в /app/data/...
|
||||||
_JSON_PATH = _REPO_ROOT / "data" / "pat" / "koltsovo_367p_subzones.json"
|
# 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-П)"
|
_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
|
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():
|
if not json_path.exists():
|
||||||
raise FileNotFoundError(f"JSON не найден: {_JSON_PATH}")
|
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)
|
data: dict[str, Any] = json.load(fh)
|
||||||
|
|
||||||
aerodrome: str = data.get("aerodrome", "Екатеринбург (Кольцово)")
|
aerodrome: str = data.get("aerodrome", "Екатеринбург (Кольцово)")
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import pathlib
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -266,3 +268,62 @@ def test_pat_lookup_graceful_programming_error() -> None:
|
||||||
db = _FakeDB(raise_exc=ProgrammingError("stmt", {}, Exception("column not found")))
|
db = _FakeDB(raise_exc=ProgrammingError("stmt", {}, Exception("column not found")))
|
||||||
result = parcel_pat_subzones(db, _WKT)
|
result = parcel_pat_subzones(db, _WKT)
|
||||||
assert result == []
|
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