diff --git a/backend/app/workers/tasks/pat_subzones_load.py b/backend/app/workers/tasks/pat_subzones_load.py index a402736e..a0b2dc07 100644 --- a/backend/app/workers/tasks/pat_subzones_load.py +++ b/backend/app/workers/tasks/pat_subzones_load.py @@ -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: код в /backend/app/workers/tasks/, данные в /data/... +# parents[4] от /backend/app/workers/tasks/ = /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", "Екатеринбург (Кольцово)") diff --git a/backend/tests/services/site_finder/test_pat_subzones.py b/backend/tests/services/site_finder/test_pat_subzones.py index 87cb9e5f..8401559b 100644 --- a/backend/tests/services/site_finder/test_pat_subzones.py +++ b/backend/tests/services/site_finder/test_pat_subzones.py @@ -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)