fix(sf): робастный путь к koltsovo JSON в pat-loader (#1155)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m9s
Deploy / build-worker (push) Successful in 3m23s
Deploy / deploy (push) Successful in 1m8s

Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
This commit is contained in:
bot-backend 2026-06-07 15:34:10 +00:00 committed by bot-reviewer
parent 08acc25c43
commit 515ac89eaa
2 changed files with 100 additions and 7 deletions

View file

@ -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", "Екатеринбург (Кольцово)")

View file

@ -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)