From 515ac89eaa83d8359dbe30c823242d012647bdb1 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 7 Jun 2026 15:34:10 +0000 Subject: [PATCH] =?UTF-8?q?fix(sf):=20=D1=80=D0=BE=D0=B1=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BF=D1=83=D1=82=D1=8C=20=D0=BA=20koltso?= =?UTF-8?q?vo=20JSON=20=D0=B2=20pat-loader=20(#1155)=20Co-authored-by:=20b?= =?UTF-8?q?ot-backend=20=20Co-committed-by:=20b?= =?UTF-8?q?ot-backend=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/workers/tasks/pat_subzones_load.py | 46 +++++++++++--- .../services/site_finder/test_pat_subzones.py | 61 +++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) 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)