chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s

This commit is contained in:
bot-backend 2026-08-21 12:01:52 +00:00
parent 1782dae0e2
commit cdf493f345
161 changed files with 958 additions and 1034 deletions

View file

@ -49,9 +49,7 @@ class OwnPlannedProjectCreate(BaseModel):
planned_release_month: date | None = Field(
None, description="Планируемый месяц выхода в продажу (нормализуется к 1-му числу)"
)
price_min_per_m2: float | None = Field(
None, ge=0, description="Нижняя граница цены, ₽/м² (≥0)"
)
price_min_per_m2: float | None = Field(None, ge=0, description="Нижняя граница цены, ₽/м² (≥0)")
price_max_per_m2: float | None = Field(
None, ge=0, description="Верхняя граница цены, ₽/м² (≥0)"
)

View file

@ -316,9 +316,7 @@ def refresh_ddu_price_indicator(db: Session, *, concurrently: bool = True) -> in
db.commit()
except OperationalError as e:
if concurrently and "cannot refresh materialized view" in str(e).lower():
logger.warning(
"ddu_indicator CONCURRENTLY failed (MV not populated), falling back"
)
logger.warning("ddu_indicator CONCURRENTLY failed (MV not populated), falling back")
db.rollback()
db.execute(text("REFRESH MATERIALIZED VIEW mv_ddu_price_indicator"))
db.commit()

View file

@ -665,8 +665,7 @@ def prinzip_insights() -> dict[str, Any]:
{
"district": "Чкаловский / Железнодорожный",
"why": (
"Растущие районы, 0% PRINZIP, низкая конкуренция. "
"Тест 60-80 м² без премиума."
"Растущие районы, 0% PRINZIP, низкая конкуренция. Тест 60-80 м² без премиума."
),
},
],
@ -688,7 +687,7 @@ def prinzip_insights() -> dict[str, Any]:
{
"name": "Холдинг Форум-групп",
"model": (
"113 тыс м² × sold 54% × Δ +21пп лидер velocity. " "3-к доля 21.5%, ср. 61 м²."
"113 тыс м² × sold 54% × Δ +21пп лидер velocity. 3-к доля 21.5%, ср. 61 м²."
),
},
],
@ -1874,7 +1873,7 @@ def _active_competitors_count(
# #38: реальный obj_class в приоритете, иначе obj_class_fallback.
if target_class:
n = _q(
"AND district_name = :dn" " AND COALESCE(obj_class, obj_class_fallback) = :cls",
"AND district_name = :dn AND COALESCE(obj_class, obj_class_fallback) = :cls",
{"rc": region_code, "dn": district_name, "cls": target_class},
)
if n >= 2:

View file

@ -251,9 +251,7 @@ def _render_what_to_build(report: dict[str, Any]) -> tuple[str, list[str]]:
if summary:
lines.append(str(summary))
if not any(
section.get(k) for k in ("obj_class", "mix", "commercial", "usp", "summary")
):
if not any(section.get(k) for k in ("obj_class", "mix", "commercial", "usp", "summary")):
lines.append("Раздел рекомендации продукта в отчёте пуст.")
return _assemble(lines), sections_used

View file

@ -229,8 +229,7 @@ def run_crossload(db: Session | None = None) -> dict[str, Any]:
except Exception as exc:
skipped += 1
logger.warning(
"etl_newbuilding_crossload: upsert failed "
"source=%s ext_id=%s: %s",
"etl_newbuilding_crossload: upsert failed source=%s ext_id=%s: %s",
params.get("source"),
params.get("ext_house_id"),
exc,

View file

@ -270,9 +270,7 @@ def _build_scenarios(doc: _DocxDocument, report: dict[str, Any]) -> None:
for name, payload in by_scenario.items():
data = _as_dict(payload)
rate_path = _as_dict(data.get("rate_path"))
rate_str = (
", ".join(f"{k}: {_fmt(v)}" for k, v in rate_path.items()) if rate_path else None
)
rate_str = ", ".join(f"{k}: {_fmt(v)}" for k, v in rate_path.items()) if rate_path else None
rows.append([name, _scenario_deficit_cell(data), rate_str, data.get("advisory")])
headers = [

View file

@ -85,8 +85,7 @@ _CONCEPT_FOOTPRINT_STYLE = {
}
_MAP_UNAVAILABLE_HTML = (
'<div class="map-placeholder">Карта недоступна — геоданные участка отсутствуют '
"в отчёте</div>"
'<div class="map-placeholder">Карта недоступна — геоданные участка отсутствуют в отчёте</div>'
)

View file

@ -348,9 +348,7 @@ def compute_affordability(
# Иначе сценарный платёж считался бы по «голой» key_rate (≈ на 4.5 п.п.
# ниже базовой ставки) и был бы НЕсопоставим с monthly_payment_rub (#1639).
market_scenario_rate = (
scenario_rate + _KEY_RATE_MARKET_SPREAD_PP
if scenario_rate is not None
else None
scenario_rate + _KEY_RATE_MARKET_SPREAD_PP if scenario_rate is not None else None
)
payment = _annuity(principal, market_scenario_rate, _ANNUITY_TERM_MONTHS)
if payment is not None:

View file

@ -123,7 +123,7 @@ def get_house_type(section_type: str) -> HouseType:
return _BY_KEY[section_type]
except KeyError as exc:
raise KeyError(
f"unknown house type {section_type!r}; " f"available: {', '.join(sorted(_BY_KEY))}"
f"unknown house type {section_type!r}; available: {', '.join(sorted(_BY_KEY))}"
) from exc

View file

@ -316,8 +316,7 @@ def parse_parcel(
raise ParcelGeometryError("buildable area degenerated after setback")
if buildable.area < MIN_BUILDABLE_AREA_SQM:
raise ParcelGeometryError(
f"buildable area {buildable.area:.1f} sqm below minimum "
f"{MIN_BUILDABLE_AREA_SQM} sqm"
f"buildable area {buildable.area:.1f} sqm below minimum {MIN_BUILDABLE_AREA_SQM} sqm"
)
effective_step = _coarsen_step_for_budget(buildable, grid_step_m)

View file

@ -231,9 +231,7 @@ def _call_with_retries(
# #1209: cap И серверное Retry-After (раньше min(...,30) применялся
# только к exp.backoff). _MAX_BACKOFF_S — единый потолок для обеих
# веток, защищает anyio-threadpool от blocking на часы.
raw_wait = float(
e.retry_after if e.retry_after is not None else 2**attempt
)
raw_wait = float(e.retry_after if e.retry_after is not None else 2**attempt)
wait = min(raw_wait, _MAX_BACKOFF_S)
logger.warning(
"llm: HTTP %s (attempt %d/%d), backing off %.1fs (raw=%.1fs)",

View file

@ -156,9 +156,7 @@ def _quarter_from_text(row_text: str) -> tuple[int, int] | None:
def build_card_url(org_id: int) -> str:
"""URL карточки организации в реестре ФАС (грид публикаций форм 14 / 4_6)."""
return (
f"{_CARD_URL}?reg={_REG}&orgId={org_id}" f"&sphere=WARM&razdel=QUARTER&form={_CARD_FORMS}"
)
return f"{_CARD_URL}?reg={_REG}&orgId={org_id}&sphere=WARM&razdel=QUARTER&form={_CARD_FORMS}"
def build_template_url(guid: str, pub_id: str) -> str:

View file

@ -357,9 +357,7 @@ def compute_gate_verdict(
warnings.append(
Warning(
code="ZOUIT_CAD_SZZ",
detail=(
f"СЗЗ ({overlap.get('type_zone', '')}): " f"{overlap.get('name', '')}"
),
detail=(f"СЗЗ ({overlap.get('type_zone', '')}): {overlap.get('name', '')}"),
)
)
elif net_kind is not None or any(
@ -376,8 +374,7 @@ def compute_gate_verdict(
Warning(
code="ZOUIT_CAD_OTHER",
detail=(
f"ЗОУИТ cad ({overlap.get('type_zone', '')}): "
f"{overlap.get('name', '')}"
f"ЗОУИТ cad ({overlap.get('type_zone', '')}): {overlap.get('name', '')}"
),
)
)

View file

@ -943,8 +943,7 @@ def compute_offer_price_trend(
delta_pct = (last_median - first_median) / first_median * 100.0
logger.info(
"offer_price_trend: lat=%.5f lon=%.5f radius=%d snapshots=%d "
"lots_latest=%s delta_pct=%s",
"offer_price_trend: lat=%.5f lon=%.5f radius=%d snapshots=%d lots_latest=%s delta_pct=%s",
center_lat,
center_lon,
radius_m,

View file

@ -91,10 +91,10 @@ def _build_overpass_query(key: str, value: str, el_type: str) -> str:
bbox = f"({south},{west},{north},{east})"
if el_type == "nwr":
# node + way: точки подключения бывают и точкой, и площадкой
return f"[out:json][timeout:30];" f'nwr["{key}"="{value}"]{bbox};' f"out geom;"
return f'[out:json][timeout:30];nwr["{key}"="{value}"]{bbox};out geom;'
if el_type == "way":
return f"[out:json][timeout:30];" f'way["{key}"="{value}"]{bbox};' f"out geom;"
return f"[out:json][timeout:30];" f'node["{key}"="{value}"]{bbox};' f"out body;"
return f'[out:json][timeout:30];way["{key}"="{value}"]{bbox};out geom;'
return f'[out:json][timeout:30];node["{key}"="{value}"]{bbox};out body;'
async def fetch_overpass_noise() -> list[dict]:

View file

@ -11,7 +11,7 @@ from app.core.db import SessionLocal
logger = logging.getLogger(__name__)
PKK6_URL = "https://pkk.rosreestr.ru/arcgis/rest/services/PKK6/ZONES/" "MapServer/5/query"
PKK6_URL = "https://pkk.rosreestr.ru/arcgis/rest/services/PKK6/ZONES/MapServer/5/query"
# bbox ЕКБ: (xmin, ymin, xmax, ymax) в WGS84
EKB_BBOX = (60.5, 56.7, 60.75, 56.95)

View file

@ -352,7 +352,7 @@ def sync_objective_group(
db.rollback()
reports_failed += 1
logger.exception(
"sync_objective_group: parser failed for %s/%s/%s " "raw_id=%s: %s",
"sync_objective_group: parser failed for %s/%s/%s raw_id=%s: %s",
section,
rtype,
rname,

View file

@ -219,9 +219,7 @@ def summarise(results: list[VectorizeResult]) -> None:
total_raster = sum(r.raster_bytes for r in results)
total_svg = sum(r.svg_bytes for r in results)
agg_ratio = total_raster / total_svg if total_svg else float("inf")
print(
f"aggregate : {total_raster}B raster -> {total_svg}B svg " f"({agg_ratio:.2f}x overall)"
)
print(f"aggregate : {total_raster}B raster -> {total_svg}B svg ({agg_ratio:.2f}x overall)")
def build_parser() -> argparse.ArgumentParser:

View file

@ -58,9 +58,9 @@ def test_nspd_zone_counts_as_known() -> None:
res = _confidence(nspd_zoning={"zone_code": "Ж-5"})
assert res["breakdown"]["zoning"] == 1.0
assert not any(
_CAVEAT in c for c in res["caveats"]
), "оговорка «зона неизвестна» при известной зоне Ж-5 — ровно то, что видел прод"
assert not any(_CAVEAT in c for c in res["caveats"]), (
"оговорка «зона неизвестна» при известной зоне Ж-5 — ровно то, что видел прод"
)
def test_regulation_zone_index_also_counts() -> None:
@ -137,11 +137,11 @@ def test_analyze_does_not_claim_unknown_zone_when_nspd_resolved_it() -> None:
app.dependency_overrides.clear()
_stop_patches()
assert (body.get("nspd_zoning") or {}).get(
"zone_code"
) == "Ж-5", "предусловие теста не выполнено: зона не доехала до ответа"
assert (body.get("nspd_zoning") or {}).get("zone_code") == "Ж-5", (
"предусловие теста не выполнено: зона не доехала до ответа"
)
caveats = " ".join(body.get("confidence_caveats") or [])
assert (
_CAVEAT not in caveats
), "ответ показывает зону Ж-5 и одновременно заявляет, что зона неизвестна"
assert _CAVEAT not in caveats, (
"ответ показывает зону Ж-5 и одновременно заявляет, что зона неизвестна"
)
assert (body.get("confidence_breakdown") or {}).get("zoning") == 1.0

View file

@ -136,9 +136,9 @@ def test_water_not_reported_as_noise_source() -> None:
noise = body.get("noise") or {}
sources = noise.get("nearby_sources") or noise.get("sources") or []
types = {s.get("source_type") for s in sources}
assert (
"water" not in types and "utility" not in types
), f"нешумовой слой попал в источники шума: {sources}"
assert "water" not in types and "utility" not in types, (
f"нешумовой слой попал в источники шума: {sources}"
)
def test_no_false_map_not_loaded_caveat_when_only_water_nearby() -> None:

View file

@ -53,9 +53,9 @@ def test_noise_no_longer_feeds_the_risk_label() -> None:
"""
блок = _risks_block_source()
assert "noise_db_max" not in блок, f"шум по-прежнему участвует в риск-блоке:\n{блок[:400]}"
assert not re.search(
r'"(high|medium|low)"', блок
), f"в риск-блоке остались словесные градации риска:\n{блок[:400]}"
assert not re.search(r'"(high|medium|low)"', блок), (
f"в риск-блоке остались словесные градации риска:\n{блок[:400]}"
)
def test_noise_score_itself_is_preserved() -> None:

View file

@ -90,9 +90,9 @@ class TestCompetitorsHaveStatusFields:
competitors = [dict(r.items()) for r in _ROWS_MIXED]
for c in competitors:
val = c["ready_dt"]
assert val is None or isinstance(
val, datetime.date
), f"ready_dt имеет неожиданный тип {type(val)}: {val}"
assert val is None or isinstance(val, datetime.date), (
f"ready_dt имеет неожиданный тип {type(val)}: {val}"
)
class TestCompetitorsSortOrder:
@ -110,9 +110,9 @@ class TestCompetitorsSortOrder:
sorted_rows = sorted(_ROWS_MIXED, key=_sort_key)
first = dict(sorted_rows[0].items())
assert (
first["site_status"] == "Строящиеся"
), f"Первый конкурент должен быть 'Строящиеся', но получили '{first['site_status']}'"
assert first["site_status"] == "Строящиеся", (
f"Первый конкурент должен быть 'Строящиеся', но получили '{first['site_status']}'"
)
def test_flat_count_desc_would_break_order(self) -> None:
"""Демонстрирует, что старый ORDER BY flat_count DESC ставил сданные первыми."""
@ -201,22 +201,22 @@ class TestObjPricingPushdown:
"""
sql = self._competitor_sql()
bounds = "WHERE oll.price_per_m2_rub BETWEEN 30000 AND 600000"
assert (
f"AVG(oll.price_per_m2_rub) FILTER ( {bounds} )" in sql
), "среднее цены должно фильтроваться границами правдоподобия (#2464-D)"
assert f"AVG(oll.price_per_m2_rub) FILTER ( {bounds} )" in sql, (
"среднее цены должно фильтроваться границами правдоподобия (#2464-D)"
)
# Тот же набор кормит счётчик выборки — иначе счётчик обещает шире, чем
# реально участвовало в среднем.
assert (
f"COUNT(*) FILTER ( {bounds} ) AS lots_with_price" in sql
), "lots_with_price должен считать ту же популяцию, что и среднее"
assert f"COUNT(*) FILTER ( {bounds} ) AS lots_with_price" in sql, (
"lots_with_price должен считать ту же популяцию, что и среднее"
)
# FILTER, а не WHERE на CTE: строки нужны целиком, иначе границы цены
# молча урежут счётчики продаж/остатка, которые считают ВСЕ лоты.
assert (
"COUNT(*) FILTER (WHERE oll.is_sold) AS units_sold" in sql
), "units_sold не должен зависеть от границ цены"
assert (
"COUNT(*) FILTER (WHERE NOT oll.is_sold) AS units_available" in sql
), "units_available не должен зависеть от границ цены"
assert "COUNT(*) FILTER (WHERE oll.is_sold) AS units_sold" in sql, (
"units_sold не должен зависеть от границ цены"
)
assert "COUNT(*) FILTER (WHERE NOT oll.is_sold) AS units_available" in sql, (
"units_available не должен зависеть от границ цены"
)
def test_obj_pricing_dedups_physflat_inline(self) -> None:
"""#1964: obj_pricing агрегирует physflat-дедуп набор (DISTINCT ON), НЕ сырой.
@ -234,9 +234,9 @@ class TestObjPricingPushdown:
"\n", " "
), "obj_lots_latest должен дедупить по physflat-ключу"
assert "snapshot_date DESC, ol.id DESC" in sql, "берём последний снапшот физлота"
assert (
"v_objective_lots_latest" not in sql
), "request-path: view материализует всю таблицу — нужен inline DISTINCT ON (#1964)"
assert "v_objective_lots_latest" not in sql, (
"request-path: view материализует всю таблицу — нужен inline DISTINCT ON (#1964)"
)
class TestCompetitorAvgAreaPd:

View file

@ -285,9 +285,9 @@ def test_inline_weights_rejects_nan() -> None:
content=raw_body,
headers={"Content-Type": "application/json"},
)
assert (
resp.status_code == 422
), f"Ожидали 422 для NaN-weight, получили {resp.status_code}: {resp.text}"
assert resp.status_code == 422, (
f"Ожидали 422 для NaN-weight, получили {resp.status_code}: {resp.text}"
)
finally:
app.dependency_overrides.clear()
_stop_patches()

View file

@ -215,9 +215,7 @@ def test_list_insights_filter_confidential_false() -> None:
listing = InsightList(total=0, limit=50, offset=0, rows=[])
with patch("app.api.v1.insights.list_insights", return_value=listing) as mock_list:
client = TestClient(app)
resp = client.get(
"/api/v1/insights", params={"is_confidential": "false"}, headers=_AUTH
)
resp = client.get("/api/v1/insights", params={"is_confidential": "false"}, headers=_AUTH)
assert resp.status_code == 200, resp.text
assert mock_list.call_args.kwargs["is_confidential"] is False
@ -261,9 +259,7 @@ def test_put_insight_returns_updated() -> None:
def test_put_insight_not_found_returns_404() -> None:
with patch("app.api.v1.insights.update_insight", return_value=None):
client = TestClient(app)
resp = client.put(
"/api/v1/insights/999", json={"title": "x"}, headers=_AUTH
)
resp = client.put("/api/v1/insights/999", json={"title": "x"}, headers=_AUTH)
assert resp.status_code == 404, resp.text

View file

@ -65,9 +65,9 @@ class TestBuildMarketPulseHonesty:
)
assert pulse["competitors_total"] == true_total
assert pulse["competitors_total"] != len(
rows
), "regression guard: competitors_total НЕ должен деградировать до len(competitor_rows)"
assert pulse["competitors_total"] != len(rows), (
"regression guard: competitors_total НЕ должен деградировать до len(competitor_rows)"
)
def test_coverage_pct_computed_against_true_total_not_capped_list(self) -> None:
"""coverage_pct = priced / TRUE total — раньше делилось на len(rows) (капнутый
@ -182,9 +182,9 @@ class TestNeighborsSummaryHonesty:
summary = parcels_module._neighbors_summary(db, "POINT(60.6 56.8)", "66:41:0000000:999")
assert summary["count_buildings_100m"] == true_total
assert summary["count_buildings_100m"] != len(
neighbors
), "regression guard: count_buildings_100m НЕ должен деградировать до len(neighbor_rows)"
assert summary["count_buildings_100m"] != len(neighbors), (
"regression guard: count_buildings_100m НЕ должен деградировать до len(neighbor_rows)"
)
assert summary["neighbors_truncated"] is True
def test_neighbors_list_itself_unaffected_by_count_fix(self) -> None:

View file

@ -77,9 +77,7 @@ def _make_out(
def test_create_own_project_returns_201_and_sets_created_by() -> None:
"""POST → 201; created_by берётся из X-Authenticated-User, не из тела."""
expected = _make_out()
with patch(
"app.api.v1.own_projects.create_own_project", return_value=expected
) as mock_create:
with patch("app.api.v1.own_projects.create_own_project", return_value=expected) as mock_create:
client = TestClient(app)
resp = client.post(
"/api/v1/own-projects",
@ -106,9 +104,7 @@ def test_create_own_project_with_unit_mix() -> None:
"""unit_mix в теле → пробрасывается в payload сервиса."""
mix = {"studio": 0.3, "1k": 0.4, "2k": 0.2, "3k": 0.1}
expected = _make_out(unit_mix=mix)
with patch(
"app.api.v1.own_projects.create_own_project", return_value=expected
) as mock_create:
with patch("app.api.v1.own_projects.create_own_project", return_value=expected) as mock_create:
client = TestClient(app)
resp = client.post(
"/api/v1/own-projects",
@ -183,9 +179,7 @@ def test_create_own_project_without_auth_header_returns_401() -> None:
def test_list_own_projects_returns_envelope() -> None:
"""GET → OwnPlannedProjectList {total, limit, offset, rows}."""
listing = OwnPlannedProjectList(
total=2, limit=50, offset=0, rows=[_make_out(1), _make_out(2)]
)
listing = OwnPlannedProjectList(total=2, limit=50, offset=0, rows=[_make_out(1), _make_out(2)])
with patch("app.api.v1.own_projects.list_own_projects", return_value=listing):
client = TestClient(app)
resp = client.get("/api/v1/own-projects", headers=_AUTH)
@ -199,9 +193,7 @@ def test_list_own_projects_returns_envelope() -> None:
def test_list_own_projects_passes_filters_to_service() -> None:
"""Фильтры district/obj_class/created_by → в сервис как kwargs."""
listing = OwnPlannedProjectList(total=0, limit=50, offset=0, rows=[])
with patch(
"app.api.v1.own_projects.list_own_projects", return_value=listing
) as mock_list:
with patch("app.api.v1.own_projects.list_own_projects", return_value=listing) as mock_list:
client = TestClient(app)
resp = client.get(
"/api/v1/own-projects",
@ -240,9 +232,7 @@ def test_put_own_project_returns_updated() -> None:
updated = _make_out(name="Переименовано")
with patch("app.api.v1.own_projects.update_own_project", return_value=updated):
client = TestClient(app)
resp = client.put(
"/api/v1/own-projects/1", json={"name": "Переименовано"}, headers=_AUTH
)
resp = client.put("/api/v1/own-projects/1", json={"name": "Переименовано"}, headers=_AUTH)
assert resp.status_code == 200, resp.text
assert resp.json()["name"] == "Переименовано"

View file

@ -449,9 +449,9 @@ def test_competitors_avg_price_populated() -> None:
)
assert resp.status_code == 200, resp.text
comp = resp.json()["competitors"][0]
assert comp["avg_price_per_m2"] == pytest.approx(
150_000.0
), "avg_price_per_m2 должен быть не None — регрессия #227 status='sold' filter"
assert comp["avg_price_per_m2"] == pytest.approx(150_000.0), (
"avg_price_per_m2 должен быть не None — регрессия #227 status='sold' filter"
)
# OBJ-3 #307: domrf-hit → price_source='domrf'.
assert comp["price_source"] == "domrf"
finally:
@ -705,9 +705,9 @@ def test_sold_count_sql_is_fanout_safe() -> None:
objective_lot_id).
"""
sql = _sold_sql_text()
assert (
"COUNT(DISTINCT objective_lot_id)" in sql
), "fan-out guard: маппинг не unique по domrf_obj_id — нужен COUNT(DISTINCT lot)"
assert "COUNT(DISTINCT objective_lot_id)" in sql, (
"fan-out guard: маппинг не unique по domrf_obj_id — нужен COUNT(DISTINCT lot)"
)
# COUNT(*) допустим внутри как агрегат? нет — sold-count агрегирует только distinct lot.
assert "COUNT(*)" not in sql, "COUNT(*) задвоит лоты при 1:N маппинге"

View file

@ -104,9 +104,9 @@ class TestNeighborsSummarySql:
for kw in forbidden_aliases:
# ищем паттерн ``WITH <kw> AS (`` или ``, <kw> AS (`` — оба
# формы CTE-биндинга.
assert (
f"with {kw} as (" not in raw_sql and f", {kw} as (" not in raw_sql
), f"CTE alias '{kw}' пересекается с PG keyword (см. incident #1195)"
assert f"with {kw} as (" not in raw_sql and f", {kw} as (" not in raw_sql, (
f"CTE alias '{kw}' пересекается с PG keyword (см. incident #1195)"
)
# ── parcel_ird_overlaps SQL ──────────────────────────────────────────────────

View file

@ -121,9 +121,9 @@ def test_prod_deploy_declares_shared_concurrency_group(name: str) -> None:
группы снова разрешат параллельный запуск.
"""
conc = yaml.safe_load(_text(name)).get("concurrency") or {}
assert (
conc.get("group") == "deploy-prod"
), f"{name}: группа concurrency = {conc.get('group')!r}, ожидалась общая 'deploy-prod'"
assert conc.get("group") == "deploy-prod", (
f"{name}: группа concurrency = {conc.get('group')!r}, ожидалась общая 'deploy-prod'"
)
assert conc.get("cancel-in-progress") is False, (
f"{name}: cancel-in-progress должен быть false — отменённый деплой оставляет "
"прод на старом коде ровно так же, как упавший"

View file

@ -41,7 +41,7 @@ FIXTURE_KEYRATE_XML = (
# где угодно), несмотря на schema-блок и namespace на KR.
FIXTURE_KEYRATE_DIFFGRAM = (
'<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
"<soap:Body><KeyRateResponse xmlns=\"http://web.cbr.ru/\"><KeyRateResult>"
'<soap:Body><KeyRateResponse xmlns="http://web.cbr.ru/"><KeyRateResult>'
'<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"'
' xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">'
'<KeyRate xmlns="">'

View file

@ -129,9 +129,9 @@ def test_income_extracts_sverdlovsk_only() -> None:
assert all(r.indicator_type == "income_per_capita" for r in rows)
assert all(r.unit == "руб" for r in rows)
assert all(r.frequency == "quarterly" for r in rows)
assert all(
r.period_type == "quarter" for r in rows
), "квартальные строки должны иметь period_type='quarter'"
assert all(r.period_type == "quarter" for r in rows), (
"квартальные строки должны иметь period_type='quarter'"
)
def test_income_concrete_values_and_dates() -> None:

View file

@ -602,6 +602,6 @@ async def test_list_objects_in_building_real() -> None:
assert listing.objdoc_id == 42065602
assert listing.flats_count > 150, f"Ожидали >150 помещений, получили {listing.flats_count}"
assert (
len(listing.flats_cad_nums) > 150
), f"Ожидали >150 cad_nums помещений, получили {len(listing.flats_cad_nums)}"
assert len(listing.flats_cad_nums) > 150, (
f"Ожидали >150 cad_nums помещений, получили {len(listing.flats_cad_nums)}"
)

View file

@ -250,9 +250,9 @@ class TestGetFeaturesInBboxGrid:
return [good_feat]
result = self._grid(_wms)
assert any(
f.feature_id == "feat-ok" for f in result
), "успешные ячейки должны попасть в результат, даже если часть слоя упала"
assert any(f.feature_id == "feat-ok" for f in result), (
"успешные ячейки должны попасть в результат, даже если часть слоя упала"
)
def test_returns_nspd_feature_instances(self) -> None:
"""Метод возвращает list[NSPDFeature] а не NSPDBulkFeature."""
@ -346,9 +346,9 @@ class TestClassifyEngineeringKind:
],
)
def test_classify(self, props: dict[str, Any], expected: str) -> None:
assert (
classify_engineering_kind(props) == expected
), f"props={props!r} → expected {expected!r}"
assert classify_engineering_kind(props) == expected, (
f"props={props!r} → expected {expected!r}"
)
def test_field_priority_params_name_over_purpose(self) -> None:
"""params_name проверяется раньше purpose."""
@ -422,13 +422,13 @@ class TestFetchLayerDispatch:
called_layer_ids = [call.args[0] for call in mock_grid.call_args_list]
from app.services.scrapers.nspd_client import LAYERS
assert (
LAYERS["territorial_zones"] in called_layer_ids
), "territorial_zones должен использовать grid-walk"
assert LAYERS["territorial_zones"] in called_layer_ids, (
"territorial_zones должен использовать grid-walk"
)
assert LAYERS["red_lines"] in called_layer_ids, "red_lines должен использовать grid-walk"
assert (
LAYERS["engineering_structures"] in called_layer_ids
), "engineering_structures должен использовать grid-walk"
assert LAYERS["engineering_structures"] in called_layer_ids, (
"engineering_structures должен использовать grid-walk"
)
# parcels и buildings — legacy, не grid
called_legacy_ids = [call.args[0] for call in mock_legacy.call_args_list]
assert LAYERS["parcels"] in called_legacy_ids, "parcels должен идти через legacy"

View file

@ -42,7 +42,7 @@ CONSTRUCTION_XLSX = (_FIXTURES / "rosstat_construction_stroitel.xlsx").read_byte
FIXTURE_META = (
"property,value\n"
"standardversion,https://rosstat.gov.ru/storage/.../versiya_4.0.pdf\n"
'identifier,7708234640-population\n'
"identifier,7708234640-population\n"
'title,"Численность постоянного населения"\n'
"created,20180905\n"
"modified,20140919\n"
@ -58,7 +58,7 @@ FIXTURE_META = (
# значение в 'total', год в 'year'. Запятая — разделитель полей; значения — целые.
FIXTURE_POPULATION_DATA = (
"number,year,kode,region,municipalities,total,urban,rural\n"
'1,2014,7900000000,Республика Адыгея,Муниципальные образования Республики Адыгеи,'
"1,2014,7900000000,Республика Адыгея,Муниципальные образования Республики Адыгеи,"
"446406,209929,236477\n"
"2,2014,6500000000,Свердловская область,Муниципальные образования Свердловской области,"
"4320677,3500000,820677\n"

View file

@ -60,8 +60,16 @@ def test_list_runs_for_light_projection_base_table_order_limit() -> None:
# LIGHT: result-блоб НЕ выбирается
assert "RESULT" not in upper, "list_runs_for НЕ должен тянуть тяжёлый result"
# выбраны именно метаданные
for col in ("ID", "CAD_NUM", "CREATED_AT", "STATUS", "SCHEMA_VERSION",
"DISTRICT", "CONFIDENCE", "CREATED_BY"):
for col in (
"ID",
"CAD_NUM",
"CREATED_AT",
"STATUS",
"SCHEMA_VERSION",
"DISTRICT",
"CONFIDENCE",
"CREATED_BY",
):
assert col in upper, f"ожидали колонку {col} в LIGHT-проекции"
assert "ORDER BY CREATED_AT DESC" in flat
assert "LIMIT CAST(:LIMIT AS INTEGER)" in upper

View file

@ -102,9 +102,9 @@ def test_ordinary_error_still_skips_only_that_quarter() -> None:
"""
outcome, touched = _run(RuntimeError("битый ответ одного квартала"))
assert not isinstance(
outcome, BaseException
), f"обычная ошибка обрушила весь прогон: {outcome!r}"
assert not isinstance(outcome, BaseException), (
f"обычная ошибка обрушила весь прогон: {outcome!r}"
)
assert len(touched) == 3, f"тронуто кварталов {len(touched)}, ожидалось 3: {touched}"

View file

@ -118,7 +118,7 @@ def test_document_carries_the_measured_window_not_the_norm() -> None:
html = _build_html([_variant(schedule_is_default=False, sales_months=54.0)])
assert (
"распродажа 54 мес" in html
), "в методической сноске стоит не тот срок, по которому посчитан NPV"
assert "распродажа 54 мес" in html, (
"в методической сноске стоит не тот срок, по которому посчитан NPV"
)
assert "распродажа 30 мес," not in html

View file

@ -101,9 +101,9 @@ def test_record_count_is_still_shown(pairs_fn: Any, fmt: str) -> None:
"""Контроль: само число записей из отчёта не пропало — подпись правится, не значение."""
pairs = pairs_fn()
counters = [(k, v) for k, v in pairs if re.fullmatch(r"\d+", v.strip())]
assert any(
int(v) == _RECORDS for _, v in counters
), f"{fmt}: число ЗОУИТ-записей ({_RECORDS}) исчезло из сводки: {counters}"
assert any(int(v) == _RECORDS for _, v in counters), (
f"{fmt}: число ЗОУИТ-записей ({_RECORDS}) исчезло из сводки: {counters}"
)
@pytest.mark.parametrize("pairs_fn,fmt", [(_html_pairs, "html"), (_docx_pairs, "docx")])
@ -111,6 +111,6 @@ def test_types_are_still_listed(pairs_fn: Any, fmt: str) -> None:
"""Контроль: перечисление типов на месте — читатель по-прежнему видит, какие они."""
pairs = pairs_fn()
joined = " ".join(f"{k} {v}" for k, v in pairs)
assert (
"Охранная зона ЛЭП" in joined and "Приаэродромная территория" in joined
), f"{fmt}: типы ЗОУИТ пропали из сводки"
assert "Охранная зона ЛЭП" in joined and "Приаэродромная территория" in joined, (
f"{fmt}: типы ЗОУИТ пропали из сводки"
)

View file

@ -48,9 +48,9 @@ def test_label_no_longer_claims_a_flood_verdict() -> None:
"""Метка называет измеренное, а не вывод, которого не делали."""
html = build_full_report_html_part_a(_result(flag=False), cad="00:00:0000000:0000")
assert (
_OLD_LABEL not in html
), "метка утверждает результат проверки зон затопления, которой не было"
assert _OLD_LABEL not in html, (
"метка утверждает результат проверки зон затопления, которой не было"
)
assert "ближе 200 м" in html, "метка должна называть измеренное — близость водотока"
@ -75,6 +75,6 @@ def test_docx_twin_uses_the_same_label() -> None:
"""
from app.services.exporters import full_report_docx, full_report_html
assert (
full_report_docx.FLOOD_PROXIMITY_LABEL is full_report_html.FLOOD_PROXIMITY_LABEL
), "DOCX держит свою копию метки — форматы разъедутся при следующей правке"
assert full_report_docx.FLOOD_PROXIMITY_LABEL is full_report_html.FLOOD_PROXIMITY_LABEL, (
"DOCX держит свою копию метки — форматы разъедутся при следующей правке"
)

View file

@ -356,9 +356,9 @@ class TestContractKeysWritten:
payload_12mo = _scenario("base", deficit_12mo=0.34)
cell_value = _scenario_deficit_cell(payload_12mo)
# Для основного горизонта — голое число, не строка с «(гор. N мес)».
assert not isinstance(
cell_value, str
), f"для 12-мес горизонта ожидается скаляр, получено '{cell_value}'"
assert not isinstance(cell_value, str), (
f"для 12-мес горизонта ожидается скаляр, получено '{cell_value}'"
)
assert cell_value == 0.34
def test_overall_score_in_cells(self) -> None:

View file

@ -157,9 +157,9 @@ class TestCoverageFactor:
f = _coverage_factor(0.4)
assert "ближних ЖК" in f.note, f.note
assert "Objective" in f.note, f.note
assert (
"будущ" not in f.note
), "нота обещала «будущие проекты», хотя мерится покрытие ближних ЖК ценами"
assert "будущ" not in f.note, (
"нота обещала «будущие проекты», хотя мерится покрытие ближних ЖК ценами"
)
def test_high_coverage(self) -> None:
f = _coverage_factor(0.75)

View file

@ -150,9 +150,9 @@ class TestSeasonalFactors:
"""
months = _months(36)
adj = seasonal_factors(months, [0] * 36)
assert (
adj.n_full_years == 0
), f"expected 0 full years on all-zero series, got {adj.n_full_years}"
assert adj.n_full_years == 0, (
f"expected 0 full years on all-zero series, got {adj.n_full_years}"
)
assert adj.applied is False
assert all(f == 1.0 for f in adj.factors.values())
@ -166,9 +166,9 @@ class TestSeasonalFactors:
# Только январь-июнь каждого года ненулевые → нет полного покрытия 12 мес.
units = [10 if d.month <= 6 else 0 for d in months]
adj = seasonal_factors(months, units)
assert (
adj.n_full_years == 0
), f"partial-coverage years should not count as full, got {adj.n_full_years}"
assert adj.n_full_years == 0, (
f"partial-coverage years should not count as full, got {adj.n_full_years}"
)
assert adj.applied is False
def test_real_nonzero_series_passes_guard_and_applies(self) -> None:

View file

@ -609,9 +609,7 @@ class TestDetectCollapsed:
def test_empty_forecasts_not_collapsed(self) -> None:
# Нет данных → нет вердикта «схлопнулось» (не помечаем пустой отчёт collapsed).
cons = _scenario("conservative", forecasts=[])
base = _scenario(
"base", forecasts=[_forecast_stub(horizon=12)]
)
base = _scenario("base", forecasts=[_forecast_stub(horizon=12)])
aggr = _scenario("aggressive", forecasts=[])
assert _detect_collapsed([cons, base, aggr]) is False
@ -750,9 +748,7 @@ class TestComputeScenariosCollapseDetection:
# demand aggressive на 1e-3 от cons/base — за пределами abs_tol=1e-6.
offset = 1e-3 if call_count["n"] == 3 else 0.0
return [
_forecast_stub(
horizon=h, deficit_index=0.05, projected_demand_units=100.0 + offset
)
_forecast_stub(horizon=h, deficit_index=0.05, projected_demand_units=100.0 + offset)
for h in horizons
]

View file

@ -61,9 +61,9 @@ def test_objective_median_sql_dedups_inline_not_via_view() -> None:
sql = str(concepts._OBJECTIVE_MEDIAN_SQL)
assert "DISTINCT ON (" in sql, "должен дедупить физлоты inline"
assert "snapshot_date DESC" in sql, "берём последний снапшот физлота"
assert (
"v_objective_lots_latest" not in sql
), "request-path: view материализует всю таблицу — нужен inline DISTINCT ON (#1964)"
assert "v_objective_lots_latest" not in sql, (
"request-path: view материализует всю таблицу — нужен inline DISTINCT ON (#1964)"
)
def test_objective_median_selected_when_sample_large_enough() -> None:

View file

@ -82,9 +82,7 @@ def _teap(residential: float, gfa: float, parking: int = 10) -> TEAP:
def test_financial_revenue_includes_parking() -> None:
t = _teap(residential=1000.0, gfa=1300.0, parking=10)
model = financial.compute_financial(
teap=t, housing_class="comfort", land_cost_rub=50_000_000.0
)
model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=50_000_000.0)
# revenue = жильё (1000 * 145_000) + паркинг comfort (10 * 1_300_000).
assert model.revenue_residential_rub == 1000.0 * 145_000.0
assert model.revenue_parking_rub == 10 * 1_300_000.0
@ -93,9 +91,7 @@ def test_financial_revenue_includes_parking() -> None:
def test_financial_cost_cascade_includes_all_lines() -> None:
t = _teap(residential=1000.0, gfa=1300.0, parking=10)
model = financial.compute_financial(
teap=t, housing_class="comfort", land_cost_rub=50_000_000.0
)
model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=50_000_000.0)
# СМР = GFA*СМР + паркинг comfort*себест (10 * 1_000_000).
assert model.construction_rub == 1300.0 * 88_000.0 + 10 * 1_000_000.0
# Каждая статья каскада > 0 при ненулевых вводных.
@ -299,15 +295,18 @@ def test_financial_parking_margin_positive_for_all_classes() -> None:
cost = financial._PARKING_COST_PER_SPOT[hc] # type: ignore[index]
assert price > cost, hc
# Конкретные маржи из спецификации: econom +450k, comfort +300k, business +100k.
assert financial._PARKING_PRICE_PER_SPOT["econom"] - financial._PARKING_COST_PER_SPOT[
"econom"
] == 450_000.0
assert financial._PARKING_PRICE_PER_SPOT["comfort"] - financial._PARKING_COST_PER_SPOT[
"comfort"
] == 300_000.0
assert financial._PARKING_PRICE_PER_SPOT["business"] - financial._PARKING_COST_PER_SPOT[
"business"
] == 100_000.0
assert (
financial._PARKING_PRICE_PER_SPOT["econom"] - financial._PARKING_COST_PER_SPOT["econom"]
== 450_000.0
)
assert (
financial._PARKING_PRICE_PER_SPOT["comfort"] - financial._PARKING_COST_PER_SPOT["comfort"]
== 300_000.0
)
assert (
financial._PARKING_PRICE_PER_SPOT["business"] - financial._PARKING_COST_PER_SPOT["business"]
== 100_000.0
)
def test_financial_econom_parking_cheaper_than_business() -> None:
@ -399,12 +398,18 @@ def test_synthesize_program_zero_site_area_no_division_error() -> None:
def test_synthesize_program_sections_do_not_affect_teap() -> None:
# sections — метаданные программы; площади уже свёрнуты → ТЭП от них не зависит.
one = teap.synthesize_teap_from_program(
total_footprint_sqm=2000.0, floors=10, site_area_sqm=5000.0,
housing_class="comfort", sections=1,
total_footprint_sqm=2000.0,
floors=10,
site_area_sqm=5000.0,
housing_class="comfort",
sections=1,
)
six = teap.synthesize_teap_from_program(
total_footprint_sqm=2000.0, floors=10, site_area_sqm=5000.0,
housing_class="comfort", sections=6,
total_footprint_sqm=2000.0,
floors=10,
site_area_sqm=5000.0,
housing_class="comfort",
sections=6,
)
assert one == six

View file

@ -127,9 +127,7 @@ def test_call_cap_returns_fallback(_enabled: None, monkeypatch: pytest.MonkeyPat
"""call_index >= llm_max_calls_per_request → fallback, провайдер не вызывается."""
monkeypatch.setattr(settings, "llm_max_calls_per_request", 2)
prov = _FakeOpenAILike()
res = complete(
system_prompt="sys", payload=SafePayload(text="hi"), provider=prov, call_index=2
)
res = complete(system_prompt="sys", payload=SafePayload(text="hi"), provider=prov, call_index=2)
assert res.reason == "call_cap"
assert prov.calls == 0
@ -241,9 +239,7 @@ def test_rate_limited_retries_then_fallback(
assert prov.calls == 3
def test_rate_limited_retry_after_capped(
_enabled: None, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_rate_limited_retry_after_capped(_enabled: None, monkeypatch: pytest.MonkeyPatch) -> None:
"""#1209: серверный Retry-After (86400с при quota-exhaustion) должен капаться
_MAX_BACKOFF_S иначе time.sleep блокирует anyio-threadpool на часы.
"""

View file

@ -88,9 +88,7 @@ def test_complete_builds_request_body(monkeypatch: pytest.MonkeyPatch) -> None:
return _chat_response()
monkeypatch.setattr(OpenAIProvider, "_post", fake_post)
resp = _provider().complete(
[{"role": "user", "content": "привет"}], max_output_tokens=256
)
resp = _provider().complete([{"role": "user", "content": "привет"}], max_output_tokens=256)
assert captured["body"]["model"] == "gpt-4o-mini"
assert captured["body"]["max_tokens"] == 256

View file

@ -103,7 +103,7 @@ def test_single_waf_does_not_abort() -> None:
Ловит «починку» через abort-on-first: тогда один переходный блок стоил бы
всей ночной докачки.
"""
stats, touched = _run(lambda oid: (_waf(oid) if oid == 5 else True))
stats, touched = _run(lambda oid: _waf(oid) if oid == 5 else True)
assert len(touched) == len(_OBJ_IDS), f"прогон оборвался на одиночном блоке: {len(touched)}"
assert "aborted_on_waf" not in stats
@ -116,7 +116,7 @@ def test_streak_resets_after_success() -> None:
Без сброса два разрозненных блока плюс третий где-то в конце оборвали бы прогон.
"""
blocked = {2, 4, 6, 8, 10}
stats, touched = _run(lambda oid: (_waf(oid) if oid in blocked else True))
stats, touched = _run(lambda oid: _waf(oid) if oid in blocked else True)
assert len(touched) == len(_OBJ_IDS), f"прогон оборвался: тронуто {len(touched)}"
assert "aborted_on_waf" not in stats

View file

@ -38,9 +38,9 @@ def test_izyatie_in_title_wins_over_later_rezervirovanie() -> None:
f"{_ЗАГОЛОВОКЗЪЯТИЕ}. Изъятию подлежат участки, ранее зарезервированные "
"постановлением о резервировании земель от 12.03.2019."
)
assert (
_detect_kind(text, "изъятие") == "изъятие"
), "документ об изъятии классифицирован по случайному упоминанию в теле"
assert _detect_kind(text, "изъятие") == "изъятие", (
"документ об изъятии классифицирован по случайному упоминанию в теле"
)
def test_all_parcels_of_the_document_are_affected() -> None:

View file

@ -98,14 +98,14 @@ def test_negation_does_not_swallow_a_real_status_in_same_block() -> None:
Разбор по первому совпадению вернул бы None и потерял бы его.
"""
result = parse_catalog_flat(_html_with_badge("Квартира не продана. Статус: в продаже"))
assert (
result.get("status") == STATUS_FREE
), f"настоящий статус в блоке потерян: {result.get('status')!r}"
assert result.get("status") == STATUS_FREE, (
f"настоящий статус в блоке потерян: {result.get('status')!r}"
)
def test_ne_inside_another_word_is_not_a_negation() -> None:
"""Контроль границы слова: «не» внутри другого слова не считается отрицанием."""
result = parse_catalog_flat(_html_with_badge("Цене снижена, квартира продана"))
assert (
result.get("status") == STATUS_SOLD
), f"«не» в хвосте слова «Цене» съело настоящий статус: {result.get('status')!r}"
assert result.get("status") == STATUS_SOLD, (
f"«не» в хвосте слова «Цене» съело настоящий статус: {result.get('status')!r}"
)

View file

@ -58,9 +58,9 @@ def test_docstring_names_the_actual_behaviour() -> None:
хорошо». Ловит «починку» через вычёркивание неудобной фразы.
"""
doc = (inspect.getdoc(mod._page_contains_table) or "").lower()
assert (
"false positive" in doc or "оглавлен" in doc
), "докстрока не предупреждает о ложных срабатываниях на оглавлении"
assert "false positive" in doc or "оглавлен" in doc, (
"докстрока не предупреждает о ложных срабатываниях на оглавлении"
)
def test_toc_line_really_is_a_false_positive() -> None:
@ -97,7 +97,7 @@ def test_seed_comment_does_not_send_to_a_dead_host() -> None:
src = Path(inspect.getsourcefile(mod)).resolve().parents[3]
текст = (src / "app" / "workers" / "tasks" / "ekb_ppt_tep_sync.py").read_text(encoding="utf-8")
assert (
"НЕ СУЩЕСТВУЕТ" in текст or "не резолв" in текст.lower()
), "комментарий сида не предупреждает, что gisogd.ekburg.ru мёртв"
assert "НЕ СУЩЕСТВУЕТ" in текст or "не резолв" in текст.lower(), (
"комментарий сида не предупреждает, что gisogd.ekburg.ru мёртв"
)
assert "gisogd66.midural.ru" in текст, "не назван живой портал ГИСОГД-СО"

View file

@ -42,7 +42,7 @@ def test_extract_next_data_from_html() -> None:
def test_extract_next_data_single_quotes() -> None:
"""Тег с одинарными кавычками тоже должен парситься."""
html = "<script id='__NEXT_DATA__'>" '{"props":{"pageProps":{}}}' "</script>"
html = '<script id=\'__NEXT_DATA__\'>{"props":{"pageProps":{}}}</script>'
result = extract_next_data(html)
assert "props" in result

View file

@ -76,13 +76,7 @@ def _html_with_badge(badge_text: str, nav_text: str = "другие кварти
def _html_with_label(label: str, value: str, nav_text: str = "") -> str:
"""HTML с лейблом «Статус» и значением в следующем блоке."""
nav = f"<span>{nav_text}</span>" if nav_text else ""
return (
"<!doctype html><html><body>"
f"{nav}"
f"<span>{label}</span>"
f"<span>{value}</span>"
"</body></html>"
)
return f"<!doctype html><html><body>{nav}<span>{label}</span><span>{value}</span></body></html>"
def _html_blocks_only(blocks: list[tuple[str, str]]) -> str:

View file

@ -115,10 +115,7 @@ def test_plan_from_img_proximity_to_label() -> None:
"""<img> без plan-hint в атрибутах, но рядом с блоком «Планировка»."""
plain = f"{BASE_URL}/api/ext/file/imgabc.png"
html = (
"<!doctype html><html><body>"
"<div>Планировка</div>"
f'<img alt="" src="{plain}">'
"</body></html>"
f'<!doctype html><html><body><div>Планировка</div><img alt="" src="{plain}"></body></html>'
)
got = extract_plan_image_url(html, _collector_for(html))
assert got == plain

View file

@ -60,14 +60,14 @@ class TestUpsertObjectSqlDoUpdateSet:
# clear back to NULL, not get stuck "still problematic" forever).
sql = " ".join(str(UPSERT_OBJECT_SQL).split())
for col in _OBJECT_PREVIOUSLY_OMITTED:
assert (
f"{col} = EXCLUDED.{col}" in sql
), f"{col} should be direct `= EXCLUDED.{col}` (not COALESCE):\n{sql}"
assert f"{col} = EXCLUDED.{col}" in sql, (
f"{col} should be direct `= EXCLUDED.{col}` (not COALESCE):\n{sql}"
)
# guard against a COALESCE(EXCLUDED.col, ...) formulation, which would
# permanently pin a stale value once ever set instead of refreshing it.
assert (
f"COALESCE(EXCLUDED.{col}," not in sql
), f"{col} must not be wrapped in COALESCE (needs to be able to clear to NULL)"
assert f"COALESCE(EXCLUDED.{col}," not in sql, (
f"{col} must not be wrapped in COALESCE (needs to be able to clear to NULL)"
)
def test_all_insert_columns_covered_by_do_update_set_or_conflict_target(self) -> None:
# Locks in the class of bug (INSERT populates a column, DO UPDATE SET

View file

@ -77,9 +77,9 @@ def test_coord_swap_ekb_point_inside_bbox() -> None:
wrong_lat = float(coords[1]) # 60.61 → это вне ЕКБ если использовать как lat
wrong_lon = float(coords[0]) # 56.83 → это вне ЕКБ если использовать как lon
# Финляндия/Норвегия: lat=60.61 формально в диапазоне 5558.5 FALSE → lat перепутан
assert not (
55.0 <= wrong_lat <= 58.5 and 58.0 <= wrong_lon <= 63.0
), f"Тест ошибочно принял перепутанные координаты: wrong_lat={wrong_lat} wrong_lon={wrong_lon}"
assert not (55.0 <= wrong_lat <= 58.5 and 58.0 <= wrong_lon <= 63.0), (
f"Тест ошибочно принял перепутанные координаты: wrong_lat={wrong_lat} wrong_lon={wrong_lon}"
)
def test_coord_swap_inverted_would_fail_bbox_check() -> None:

View file

@ -66,9 +66,9 @@ def test_savepoint_is_actually_entered(name: str) -> None:
"""
db = PostgresLikeSession(fail_on=(), rows=[]) # здоровый путь
_call(name, db)
assert (
db.savepoints_entered >= 1
), f"{name}: запрос выполнен вне SAVEPOINT — при сбое сессия останется aborted"
assert db.savepoints_entered >= 1, (
f"{name}: запрос выполнен вне SAVEPOINT — при сбое сессия останется aborted"
)
assert db.calls >= 1, f"{name}: запрос вообще не выполнялся"

View file

@ -27,6 +27,6 @@ def test_select_default_is_ordered() -> None:
"""
from app.services.site_finder.weight_profiles import _SELECT_DEFAULT
assert re.search(
r"ORDER BY\s+id\s+ASC", _SELECT_DEFAULT
), f"в _SELECT_DEFAULT нет тай-брейка по id:\n{_SELECT_DEFAULT}"
assert re.search(r"ORDER BY\s+id\s+ASC", _SELECT_DEFAULT), (
f"в _SELECT_DEFAULT нет тай-брейка по id:\n{_SELECT_DEFAULT}"
)

View file

@ -85,9 +85,9 @@ def test_borrowed_session_is_not_committed_per_organization() -> None:
db, visited, n_orgs = _run(own=False)
assert len(visited) == n_orgs
assert (
db.commits == 1
), f"на чужой сессии {db.commits} коммитов — транзакцией распоряжается вызывающий"
assert db.commits == 1, (
f"на чужой сессии {db.commits} коммитов — транзакцией распоряжается вызывающий"
)
assert not db.closed, "чужая сессия закрыта — её закрывает вызывающий"

View file

@ -54,18 +54,18 @@ def test_far_is_used_when_pct_is_impossible() -> None:
"""Отбрасываем только испорченный параметр, остальной расчёт остаётся верным."""
t = _teap(max_building_pct=150.0, max_far=2.0)
assert t is not None
assert t.total_floor_area_sqm == pytest.approx(
_ПЛОЩАДЬ * 2.0
), f"GFA посчитана не по КСИТ: {t.total_floor_area_sqm}"
assert t.total_floor_area_sqm == pytest.approx(_ПЛОЩАДЬ * 2.0), (
f"GFA посчитана не по КСИТ: {t.total_floor_area_sqm}"
)
def test_impossible_far_is_dropped() -> None:
"""КСИТ 500 — признак порчи разбора, а не сверхплотной застройки."""
t = _teap(max_far=500.0, max_building_pct=40.0, max_floors=10)
assert t is not None
assert t.total_floor_area_sqm == pytest.approx(
_ПЛОЩАДЬ * 0.4 * 10
), f"использован невозможный КСИТ: GFA={t.total_floor_area_sqm}"
assert t.total_floor_area_sqm == pytest.approx(_ПЛОЩАДЬ * 0.4 * 10), (
f"использован невозможный КСИТ: GFA={t.total_floor_area_sqm}"
)
def test_impossible_floors_is_dropped() -> None:
@ -127,6 +127,6 @@ def test_footprint_never_exceeds_parcel_even_without_pct_and_floors() -> None:
f"пятно {t.built_area_sqm} на участке {_ПЛОЩАДЬ} — нарушена геометрия, "
"а не только правдоподобие регламента"
)
assert t.total_floor_area_sqm == pytest.approx(
_ПЛОЩАДЬ * 2.0
), "GFA не должна меняться от ограничения пятна"
assert t.total_floor_area_sqm == pytest.approx(_ПЛОЩАДЬ * 2.0), (
"GFA не должна меняться от ограничения пятна"
)

View file

@ -95,9 +95,9 @@ def test_label_is_independent_of_overlap_order() -> None:
"ZOUIT_NETWORK_OBREMENENIE",
)
assert d1 is not None and d2 is not None
assert (
d1["detail"] == d2["detail"]
), f"подпись зависит от порядка:\n{d1['detail']}\n{d2['detail']}"
assert d1["detail"] == d2["detail"], (
f"подпись зависит от порядка:\n{d1['detail']}\n{d2['detail']}"
)
def test_duplicate_kind_named_once() -> None:

View file

@ -543,14 +543,14 @@ def test_supply_joins_flats_per_object_latest_snapshot() -> None:
sql_text = str(_SUPPLY_BATCH_SQL.text)
# per-object последний снимок flats
assert "flats_latest" in sql_text, "нет flats_latest CTE → supply снова на глобал-max снимке"
assert (
"DISTINCT ON (f.obj_id)" in sql_text
), "flats не дедупятся per-object → нет per-obj снимка"
assert "DISTINCT ON (f.obj_id)" in sql_text, (
"flats не дедупятся per-object → нет per-obj снимка"
)
# НЕ должно быть джойна по единой внешней дате-параметру (регрессия #1944)
assert ":latest_snap" not in sql_text, "глобальный :latest_snap вернулся → supply=0 регрессия"
assert (
"MAX(snapshot_date)" not in sql_text
), "глобальный MAX(snapshot_date) → supply=0 регрессия"
assert "MAX(snapshot_date)" not in sql_text, (
"глобальный MAX(snapshot_date) → supply=0 регрессия"
)
# ── Тесты _cap_and_redistribute (Fix SF-09 review) ───────────────────────────
@ -592,16 +592,16 @@ def test_cap_and_redistribute_invariants(
"""
result, cap_skipped = _cap_and_redistribute(pct_map)
assert (
cap_skipped == expect_pathological
), f"cap_skipped={cap_skipped} но ожидали {expect_pathological} для {pct_map}"
assert (
sum(result.values()) == 100
), f"sum={sum(result.values())} != 100 для {pct_map}{result}"
assert cap_skipped == expect_pathological, (
f"cap_skipped={cap_skipped} но ожидали {expect_pathological} для {pct_map}"
)
assert sum(result.values()) == 100, (
f"sum={sum(result.values())} != 100 для {pct_map}{result}"
)
if not expect_pathological:
assert (
max(result.values()) <= MAX_BUCKET_SHARE_PCT
), f"max={max(result.values())} > cap={MAX_BUCKET_SHARE_PCT} для {pct_map}{result}"
assert max(result.values()) <= MAX_BUCKET_SHARE_PCT, (
f"max={max(result.values())} > cap={MAX_BUCKET_SHARE_PCT} для {pct_map}{result}"
)
@pytest.mark.parametrize(
@ -623,14 +623,14 @@ def test_cap_reproduced_failing_cases(
) -> None:
"""Review round-2 reproduced cases: 2-bucket — pathological, 3-bucket — fit cap."""
result, cap_skipped = _cap_and_redistribute(deals)
assert (
cap_skipped == expect_pathological
), f"cap_skipped={cap_skipped} ожидали {expect_pathological} для {label}"
assert cap_skipped == expect_pathological, (
f"cap_skipped={cap_skipped} ожидали {expect_pathological} для {label}"
)
assert sum(result.values()) == 100, f"sum != 100 для {label}{result}"
if not expect_pathological:
assert (
max(result.values()) <= MAX_BUCKET_SHARE_PCT
), f"max={max(result.values())} > {MAX_BUCKET_SHARE_PCT} для {label}{result}"
assert max(result.values()) <= MAX_BUCKET_SHARE_PCT, (
f"max={max(result.values())} > {MAX_BUCKET_SHARE_PCT} для {label}{result}"
)
def test_cap_iteration_count_bounded() -> None:
@ -773,9 +773,9 @@ def test_group_radius_objects_prod_case_groups() -> None:
assert len(groups) == 6, f"ожидалось 6 групп, получено {len(groups)}: {got}"
# load-bearing инвариант: 7 ключей и безымянные Эфесы разделены >300 м
assert frozenset({15731}) in got, "7 ключей должна быть отдельной группой"
assert (
frozenset({55320, 55321, 55322}) in got
), "3 безымянных Эфеса — один кластер, отдельный от 7 ключей"
assert frozenset({55320, 55321, 55322}) in got, (
"3 безымянных Эфеса — один кластер, отдельный от 7 ключей"
)
assert got == expected
@ -1155,9 +1155,9 @@ def test_supply_only_velocity_group_not_duplicated() -> None:
]
for call in supply_calls:
params = call.args[1] if len(call.args) > 1 else call.kwargs
assert "Траектория" not in (
params.get("names") or []
), "velocity-проект не должен уходить в supply-only запрос"
assert "Траектория" not in (params.get("names") or []), (
"velocity-проект не должен уходить в supply-only запрос"
)
def test_supply_only_empty_snapshot_yields_empty_block() -> None:

View file

@ -133,18 +133,14 @@ def _competitors_response(*obj_ids: int) -> CompetitorsResponse:
def test_parking_wire_happy_path(monkeypatch: Any) -> None:
"""top-конкурент сматчился на здание → получает parking_ratio из НСПД."""
monkeypatch.setattr(
competitors_mod, "get_competitors", lambda **_: _competitors_response(101)
)
monkeypatch.setattr(competitors_mod, "get_competitors", lambda **_: _competitors_response(101))
monkeypatch.setattr(
competitors_mod,
"resolve_cad_for_domrf",
lambda *_a, **_k: BuildingMatch("66:41:0106036:183", 40995027, 12.3),
)
listing = ObjectsListing(objdoc_id=40995027, flats_count=200, parking_count=100)
monkeypatch.setattr(
competitors_mod, "get_building_premises_for_match", lambda _m: listing
)
monkeypatch.setattr(competitors_mod, "get_building_premises_for_match", lambda _m: listing)
out = competitors_mod.get_competitors_parking(
MagicMock(), "66:41:0303001:1", CompetitorsRequest()
@ -192,9 +188,7 @@ def test_parking_wire_limits_to_top_n(monkeypatch: Any) -> None:
def test_parking_wire_no_geom_match_graceful(monkeypatch: Any) -> None:
"""geom-match промахнулся → конкурент в items с None-полями, matched_count=0."""
monkeypatch.setattr(
competitors_mod, "get_competitors", lambda **_: _competitors_response(7)
)
monkeypatch.setattr(competitors_mod, "get_competitors", lambda **_: _competitors_response(7))
monkeypatch.setattr(competitors_mod, "resolve_cad_for_domrf", lambda *_a, **_k: None)
premises = MagicMock()
monkeypatch.setattr(competitors_mod, "get_building_premises_for_match", premises)
@ -212,9 +206,7 @@ def test_parking_wire_no_geom_match_graceful(monkeypatch: Any) -> None:
def test_parking_wire_premises_none_keeps_cad(monkeypatch: Any) -> None:
"""Здание сматчилось, но НСПД premises=None → cad есть, parking_ratio None."""
monkeypatch.setattr(
competitors_mod, "get_competitors", lambda **_: _competitors_response(9)
)
monkeypatch.setattr(competitors_mod, "get_competitors", lambda **_: _competitors_response(9))
monkeypatch.setattr(
competitors_mod,
"resolve_cad_for_domrf",
@ -234,9 +226,7 @@ def test_parking_wire_premises_none_keeps_cad(monkeypatch: Any) -> None:
def test_parking_wire_premises_exception_graceful(monkeypatch: Any) -> None:
"""Неожиданное исключение в premises-lookup → конкурент без паркинга, не 500."""
monkeypatch.setattr(
competitors_mod, "get_competitors", lambda **_: _competitors_response(11)
)
monkeypatch.setattr(competitors_mod, "get_competitors", lambda **_: _competitors_response(11))
monkeypatch.setattr(
competitors_mod,
"resolve_cad_for_domrf",

View file

@ -321,9 +321,9 @@ def test_json_path_candidates_end_with_expected_suffix() -> None:
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}"
assert candidate.parts[-3:] == expected_suffix.parts, (
f"Кандидат {candidate} не заканчивается на {expected_suffix}"
)
def test_resolve_json_path_finds_real_file() -> None:

View file

@ -201,9 +201,9 @@ def test_sql_excludes_the_amendments_group() -> None:
держаться на том, что таких строк «пока нет».
"""
sql = str(_PERMITS_NEARBY_SQL)
assert (
"doc_group IN ('RS', 'RV')" in sql
), f"запрос не сужен по группе — строки 'IZ' сломают total_count:\n{sql}"
assert "doc_group IN ('RS', 'RV')" in sql, (
f"запрос не сужен по группе — строки 'IZ' сломают total_count:\n{sql}"
)
def test_total_equals_rs_plus_rv_even_if_iz_leaks_in() -> None:

View file

@ -395,12 +395,12 @@ class TestLayer2Hidden:
norm = " ".join(sql.split())
# Оба FILTER-предложения (n_with_free_flats COUNT и hidden_units SUM) должны
# гейтить flat_count IS NOT NULL наравне с free_flats IS NOT NULL.
assert (
norm.count("flat_count IS NOT NULL") == 2
), f"expected flat_count IS NOT NULL guard on both COUNT and SUM filters:\n{sql}"
assert (
norm.count("free_flats IS NOT NULL") == 2
), f"expected free_flats IS NOT NULL guard on both COUNT and SUM filters:\n{sql}"
assert norm.count("flat_count IS NOT NULL") == 2, (
f"expected flat_count IS NOT NULL guard on both COUNT and SUM filters:\n{sql}"
)
assert norm.count("free_flats IS NOT NULL") == 2, (
f"expected free_flats IS NOT NULL guard on both COUNT and SUM filters:\n{sql}"
)
def test_row_with_null_flat_count_and_positive_free_flats_excluded_end_to_end(
self,

View file

@ -56,9 +56,9 @@ def test_quarter_dump_docstring_does_not_claim_17_requests() -> None:
from app.services.scrapers.nspd_client import QuarterDump
doc = inspect.getdoc(QuarterDump) or ""
assert (
"не сжигать rate-limit на 17 запросов" not in doc
), "в докстроке осталось число 17, противоречащее grid-walk"
assert "не сжигать rate-limit на 17 запросов" not in doc, (
"в докстроке осталось число 17, противоречащее grid-walk"
)
def test_on_demand_docstring_does_not_promise_a_60s_window() -> None:
@ -105,6 +105,6 @@ def test_docstrings_state_the_actual_behaviour() -> None:
assert "include_zouit" in qd, "не назван фактический дефолт дампа"
od = inspect.getdoc(find_active_on_demand_job) or ""
assert (
"НИКОГДА" in od or "никогда" in od
), "не сказано, что failed не возвращается независимо от давности"
assert "НИКОГДА" in od or "никогда" in od, (
"не сказано, что failed не возвращается независимо от давности"
)

View file

@ -42,9 +42,9 @@ def test_water_result_keeps_period() -> None:
patch.object(mod.zipfile, "ZipFile", MagicMock()),
):
res = mod.load_water_reserves_from_docx(MagicMock(), "supply", b"", "http://x")
assert (
res.get("period") == "III кв. 2025"
), f"период выброшен из ответа: {res} — по логам он есть, у вызывающего нет"
assert res.get("period") == "III кв. 2025", (
f"период выброшен из ответа: {res} — по логам он есть, у вызывающего нет"
)
assert res.get("records") == 1 and res.get("inserted") == 1, res
@ -108,12 +108,12 @@ def test_placement_warning_uses_actual_footprint_not_catalog() -> None:
src = inspect.getsource(mod.place_program)
хвост = src[src.index("участок мал") :]
assert (
"fp_w," in хвост and "fp_d," in хвост
), f"в предупреждении не фактические габариты:\n{хвост[:320]}"
assert (
"house.footprint_w_m," not in хвост and "house.footprint_d_m," not in хвост
), f"в предупреждении остался каталожный размер:\n{хвост[:320]}"
assert "fp_w," in хвост and "fp_d," in хвост, (
f"в предупреждении не фактические габариты:\n{хвост[:320]}"
)
assert "house.footprint_w_m," not in хвост and "house.footprint_d_m," not in хвост, (
f"в предупреждении остался каталожный размер:\n{хвост[:320]}"
)
@pytest.mark.parametrize("имя", ["fp_w", "fp_d"])

View file

@ -60,7 +60,7 @@ class _PostgresLikeDb:
def execute(self, *_args: Any, **_kwargs: Any) -> Any:
if self.aborted:
raise AbortedTransactionError(
"current transaction is aborted, commands ignored until end of " "transaction block"
"current transaction is aborted, commands ignored until end of transaction block"
)
self.calls += 1
if self.calls == 1 and self._fail_first:

View file

@ -116,9 +116,9 @@ class TestActiveCompetitorsCountSqlShape:
# site_status legitimately appears in the CTE's SELECT list (it's the
# column DISTINCT ON needs to expose) -- what must NOT appear is a
# filter predicate on it inside the CTE's WHERE.
assert (
"site_status = 'Строящиеся'" not in cte_body
), f"site_status must not pre-filter the DISTINCT ON CTE (volatile field):\n{cte_body}"
assert "site_status = 'Строящиеся'" not in cte_body, (
f"site_status must not pre-filter the DISTINCT ON CTE (volatile field):\n{cte_body}"
)
assert "site_status = 'Строящиеся'" in outer_body
def test_district_and_class_are_volatile_applied_after_distinct_on(self) -> None:
@ -136,12 +136,12 @@ class TestActiveCompetitorsCountSqlShape:
)
cte_body, outer_body = _split_latest_cte(_executed_sql(db, 0))
# CTE WHERE must scope on ONLY the stable region_cd — no volatile predicate.
assert (
"district_name = :dn" not in cte_body
), f"district_name (volatile) must not pre-filter the DISTINCT ON CTE:\n{cte_body}"
assert (
"COALESCE(obj_class, obj_class_fallback) = :cls" not in cte_body
), f"obj_class (volatile) must not pre-filter the DISTINCT ON CTE:\n{cte_body}"
assert "district_name = :dn" not in cte_body, (
f"district_name (volatile) must not pre-filter the DISTINCT ON CTE:\n{cte_body}"
)
assert "COALESCE(obj_class, obj_class_fallback) = :cls" not in cte_body, (
f"obj_class (volatile) must not pre-filter the DISTINCT ON CTE:\n{cte_body}"
)
# Both live in the outer WHERE, applied to the deduped true-latest row.
assert "district_name = :dn" in outer_body
assert "COALESCE(obj_class, obj_class_fallback) = :cls" in outer_body
@ -161,9 +161,9 @@ class TestActiveCompetitorsCountSqlShape:
where_start = cte_body.index("WHERE ") + len("WHERE ")
where_end = cte_body.index("ORDER BY", where_start)
where_clause = cte_body[where_start:where_end].strip()
assert (
where_clause == "region_cd = :rc"
), f"CTE WHERE must scope on ONLY stable region_cd, got: {where_clause!r}"
assert where_clause == "region_cd = :rc", (
f"CTE WHERE must scope on ONLY stable region_cd, got: {where_clause!r}"
)
def test_no_double_colon_cast(self) -> None:
import re

View file

@ -397,12 +397,12 @@ class TestMsk66ToWgs84:
result = msk66_to_wgs84("1534814.7997", "394813.2001")
assert result is not None
lon, lat = result
assert (
abs(lon - 60.619637) < _TOL_LON_DEG
), f"lon={lon:.6f} далеко от эталонного 60.619637 (Δ={abs(lon - 60.619637):.6f}°)"
assert (
abs(lat - 56.871948) < _TOL_LAT_DEG
), f"lat={lat:.6f} далеко от эталонного 56.871948 (Δ={abs(lat - 56.871948):.6f}°)"
assert abs(lon - 60.619637) < _TOL_LON_DEG, (
f"lon={lon:.6f} далеко от эталонного 60.619637 (Δ={abs(lon - 60.619637):.6f}°)"
)
assert abs(lat - 56.871948) < _TOL_LAT_DEG, (
f"lat={lat:.6f} далеко от эталонного 56.871948 (Δ={abs(lat - 56.871948):.6f}°)"
)
def test_koltsovo_cad_crosscheck(self) -> None:
"""X=1544026.7997/Y=381585.4401 — сверка с centroid cad_parcels 66:41:0503018:248.
@ -412,12 +412,12 @@ class TestMsk66ToWgs84:
result = msk66_to_wgs84("1544026.7997", "381585.4401")
assert result is not None
lon, lat = result
assert (
abs(lon - 60.768255) < _TOL_LON_DEG
), f"lon={lon:.6f} далеко от эталонного 60.768255 (Δ={abs(lon - 60.768255):.6f}°)"
assert (
abs(lat - 56.752330) < _TOL_LAT_DEG
), f"lat={lat:.6f} далеко от эталонного 56.752330 (Δ={abs(lat - 56.752330):.6f}°)"
assert abs(lon - 60.768255) < _TOL_LON_DEG, (
f"lon={lon:.6f} далеко от эталонного 60.768255 (Δ={abs(lon - 60.768255):.6f}°)"
)
assert abs(lat - 56.752330) < _TOL_LAT_DEG, (
f"lat={lat:.6f} далеко от эталонного 56.752330 (Δ={abs(lat - 56.752330):.6f}°)"
)
def test_smorodinovaya_cad_crosscheck(self) -> None:
"""X=1526550.1397/Y=387129.3801 — сверка с centroid cad_parcels 66:41:0306057:89.
@ -427,12 +427,12 @@ class TestMsk66ToWgs84:
result = msk66_to_wgs84("1526550.1397", "387129.3801")
assert result is not None
lon, lat = result
assert (
abs(lon - 60.483224) < _TOL_LON_DEG
), f"lon={lon:.6f} далеко от эталонного 60.483224 (Δ={abs(lon - 60.483224):.6f}°)"
assert (
abs(lat - 56.803547) < _TOL_LAT_DEG
), f"lat={lat:.6f} далеко от эталонного 56.803547 (Δ={abs(lat - 56.803547):.6f}°)"
assert abs(lon - 60.483224) < _TOL_LON_DEG, (
f"lon={lon:.6f} далеко от эталонного 60.483224 (Δ={abs(lon - 60.483224):.6f}°)"
)
assert abs(lat - 56.803547) < _TOL_LAT_DEG, (
f"lat={lat:.6f} далеко от эталонного 56.803547 (Δ={abs(lat - 56.803547):.6f}°)"
)
def test_respublikanskaya_cad_crosscheck(self) -> None:
"""X=1531655.2797/Y=398430.2401 — ул. Республиканская 1а, cad 66:41:0106051:10.
@ -443,12 +443,12 @@ class TestMsk66ToWgs84:
result = msk66_to_wgs84("1531655.2797", "398430.2401")
assert result is not None
lon, lat = result
assert (
abs(lon - 60.568320) < _TOL_LON_DEG
), f"lon={lon:.6f} далеко от эталонного 60.568320 (Δ={abs(lon - 60.568320):.6f}°)"
assert (
abs(lat - 56.904674) < _TOL_LAT_DEG
), f"lat={lat:.6f} далеко от эталонного 56.904674 (Δ={abs(lat - 56.904674):.6f}°)"
assert abs(lon - 60.568320) < _TOL_LON_DEG, (
f"lon={lon:.6f} далеко от эталонного 60.568320 (Δ={abs(lon - 60.568320):.6f}°)"
)
assert abs(lat - 56.904674) < _TOL_LAT_DEG, (
f"lat={lat:.6f} далеко от эталонного 56.904674 (Δ={abs(lat - 56.904674):.6f}°)"
)
def test_kosmonavtov_in_ekb(self) -> None:
"""Пр. Космонавтов — попадает в ЕКБ-регион."""

View file

@ -162,10 +162,10 @@ def test_upsert_sql_coalesce_external_ids():
assert "COALESCE" in do_update_section, "DO UPDATE должен содержать COALESCE"
# yandex_jk_id — строка вида "YANDEX_JK_ID = COALESCE("
assert (
"YANDEX_JK_ID = COALESCE(" in do_update_section
), "yandex_jk_id в DO UPDATE должен использовать COALESCE чтобы не затирать NULL'ом"
assert "YANDEX_JK_ID = COALESCE(" in do_update_section, (
"yandex_jk_id в DO UPDATE должен использовать COALESCE чтобы не затирать NULL'ом"
)
# cian_internal_house_id — строка вида "CIAN_INTERNAL_HOUSE_ID = COALESCE("
assert (
"CIAN_INTERNAL_HOUSE_ID = COALESCE(" in do_update_section
), "cian_internal_house_id в DO UPDATE должен использовать COALESCE чтобы не затирать NULL'ом"
assert "CIAN_INTERNAL_HOUSE_ID = COALESCE(" in do_update_section, (
"cian_internal_house_id в DO UPDATE должен использовать COALESCE чтобы не затирать NULL'ом"
)

View file

@ -422,9 +422,9 @@ class TestRealisticSrokFallback:
)
srok = result["summary"]["months_to_sellout_total"]
assert srok is not None
assert (
lo <= srok <= hi
), f"n_comp={n_comp}, area={area}: срок {srok:.1f} вне [{lo}, {hi}]"
assert lo <= srok <= hi, (
f"n_comp={n_comp}, area={area}: срок {srok:.1f} вне [{lo}, {hi}]"
)
def test_scope_has_n_competitors(self) -> None:
"""scope.n_competitors присутствует и равен district+class competitors."""
@ -522,9 +522,9 @@ class TestObjectivePerBucketPath:
# Studio: macro_mult = sat_factor × trend_factor = 1.0 × 1.0 = 1.0
studio = bkt_map.get("Студии 15-30")
assert studio is not None
assert studio["velocity_per_month"] == pytest.approx(
3.5, rel=0.01
), f"Studio velocity={studio['velocity_per_month']:.3f}, ожидалось 3.5"
assert studio["velocity_per_month"] == pytest.approx(3.5, rel=0.01), (
f"Studio velocity={studio['velocity_per_month']:.3f}, ожидалось 3.5"
)
assert studio.get("velocity_source") == "objective_per_bucket"
def test_objective_velocities_vary(self) -> None:

View file

@ -553,9 +553,9 @@ class TestWindDirectionAllNone:
f"ожидался None, получено {wind['dominant_direction_deg']!r}"
"вероятно, fabricated 0.0° из-за atan2(0,0)"
)
assert (
wind["dominant_direction_label"] is None
), f"ожидался None, получено {wind['dominant_direction_label']!r}"
assert wind["dominant_direction_label"] is None, (
f"ожидался None, получено {wind['dominant_direction_label']!r}"
)
def test_missing_wind_key_gives_none_direction(self) -> None:
"""winddirection_10m_dominant отсутствует в ответе → dominant_direction_deg/label = None."""

View file

@ -106,9 +106,7 @@ def _run() -> dict[tuple[str, date], int]:
session.execute(text(_TEMP))
for cad, d, url in _SEED:
session.execute(
text(
"INSERT INTO land_reservation (cad_num, act_date, doc_url)" " VALUES (:c,:d,:u)"
),
text("INSERT INTO land_reservation (cad_num, act_date, doc_url) VALUES (:c,:d,:u)"),
{"c": cad, "d": d, "u": url},
)
for chunk in _body():

View file

@ -123,9 +123,9 @@ def test_plain_unique_does_not_deduplicate(db) -> None:
Без этой проверки зелёный тест выше неотличим от «оно и так работало».
"""
_add_constraint(db, nulls_not_distinct=False)
assert (
_insert_twice(db) == 2
), "обычный UNIQUE неожиданно поймал дубль — значит тест выше ничего не доказывает"
assert _insert_twice(db) == 2, (
"обычный UNIQUE неожиданно поймал дубль — значит тест выше ничего не доказывает"
)
def test_records_with_act_number_still_deduplicate(db) -> None:
@ -159,9 +159,9 @@ def test_migration_dedup_statement_matches_the_key(db) -> None:
assert delete_stmt is not None, "в миграции нет DELETE — дедуп не выполняется"
body = delete_stmt.group(0)
assert "a.cad_num = b.cad_num" in body, "дедуп не по cad_num"
assert (
"a.act_number IS NULL" in body and "b.act_number IS NULL" in body
), "дедуп затрагивает записи С номером акта — они и так были уникальны"
assert "a.act_number IS NULL" in body and "b.act_number IS NULL" in body, (
"дедуп затрагивает записи С номером акта — они и так были уникальны"
)
assert "a.id > b.id" in body, "не задан выживающий (минимальный id)"

View file

@ -169,9 +169,9 @@ def test_revenue_and_deals_are_named_by_their_scope(seeded) -> None:
просто перестала показывать поле в UI.
"""
stats = _stats(seeded)
assert (
stats.get("revenue_window") == _IN_WINDOW_REVENUE
), f"revenue_window = {stats.get('revenue_window')}, ожидалось {_IN_WINDOW_REVENUE}"
assert stats.get("revenue_window") == _IN_WINDOW_REVENUE, (
f"revenue_window = {stats.get('revenue_window')}, ожидалось {_IN_WINDOW_REVENUE}"
)
assert stats.get("deals_window") == _IN_WINDOW_DEALS

View file

@ -110,9 +110,9 @@ def _nspd(db) -> dict:
payload = compute_freshness(db)
rows = [s for s in payload["sources"] if s["source"] == "nspd"]
assert (
len(rows) == 1
), f"источник nspd не найден в реестре: {[s['source'] for s in payload['sources']]}"
assert len(rows) == 1, (
f"источник nspd не найден в реестре: {[s['source'] for s in payload['sources']]}"
)
return rows[0]
@ -181,7 +181,7 @@ def test_attempt_is_still_recorded(db) -> None:
assert src["last_attempt_at"] is not None
assert src["last_success_at"] is not None
assert src["last_attempt_at"] > src["last_success_at"], (
"последняя попытка должна быть новее последнего успеха — иначе провалы " "не видны вообще"
"последняя попытка должна быть новее последнего успеха — иначе провалы не видны вообще"
)

View file

@ -141,9 +141,9 @@ def test_loader_knows_the_amendments_group() -> None:
"""
from app.services.scrapers.gisogd66 import GROUP_CODE
assert (
GROUP_CODE.get("DocIZ") == "IZ"
), f"группа изменений не грузится; GROUP_CODE = {GROUP_CODE}"
assert GROUP_CODE.get("DocIZ") == "IZ", (
f"группа изменений не грузится; GROUP_CODE = {GROUP_CODE}"
)
def test_loader_upserts_by_source_key() -> None:
@ -158,9 +158,9 @@ def test_loader_upserts_by_source_key() -> None:
src = inspect.getsource(_upsert_permit)
assert "ON CONFLICT (source_key)" in src, "UPSERT конфликтует не по source_key"
assert (
"ON CONFLICT (doc_group, doc_num)" not in src
), "старый ключ всё ещё в запросе — разрешение и изменения схлопнутся"
assert "ON CONFLICT (doc_group, doc_num)" not in src, (
"старый ключ всё ещё в запросе — разрешение и изменения схлопнутся"
)
# ── Механизм и миграция: нужен живой Postgres ────────────────────────────────
@ -184,9 +184,9 @@ def test_old_key_collapses_permit_and_its_amendment() -> None:
s.execute(text(_OLD_UPSERT), ЗМЕНЕНИЕ)
rows = s.execute(text("SELECT doc_name, source_key FROM gisogd_permits")).all()
assert len(rows) == 1, f"ожидали схлопывание, получили {len(rows)} строк"
assert (
"Изменения" in rows[0][0]
), f"вытеснено не то: осталось {rows[0][0]!r} — на проде остаётся именно изменение"
assert "Изменения" in rows[0][0], (
f"вытеснено не то: осталось {rows[0][0]!r} — на проде остаётся именно изменение"
)
finally:
s.rollback()
s.close()

View file

@ -141,9 +141,9 @@ def test_schema_01_alone_is_red_for_the_publishable_quarter(sandbox) -> None:
conn, schema = sandbox
have = _partition_starts(conn, schema)
assert have, "песочница пуста — 01_schema не применилась"
assert max(have) == date(
2026, 1, 1
), f"горизонт 01-схемы ожидался 2026q1, есть {sorted(have)[-2:]}"
assert max(have) == date(2026, 1, 1), (
f"горизонт 01-схемы ожидался 2026q1, есть {sorted(have)[-2:]}"
)
need = date(2026, 4, 1) # публикуемый квартал на дату инцидента
assert need not in have, "красная сторона не состоялась: 01-схема уже знает Q2 2026"

View file

@ -115,9 +115,9 @@ def test_migrations_are_transactional() -> None:
]
if not statements or statements[0] != "BEGIN;" or statements[-1] != "COMMIT;":
broken.append(path.name)
assert (
not broken
), f"Миграции без обёртки BEGIN;/COMMIT;: {broken} (.claude/rules/sql.md → Structure)."
assert not broken, (
f"Миграции без обёртки BEGIN;/COMMIT;: {broken} (.claude/rules/sql.md → Structure)."
)
def test_no_concurrent_index_in_migrations() -> None:
@ -177,6 +177,6 @@ def test_deploy_workflow_applies_auth_migrations() -> None:
f"В {_DEPLOY_WORKFLOW.name} нет цикла по data/sql/auth/*.sql — миграции БД auth "
"не применяются на деплое."
)
assert (
"ops/db-bootstrap/create_auth_db.sql" in workflow
), f"В {_DEPLOY_WORKFLOW.name} нет bootstrap-шага создания БД auth."
assert "ops/db-bootstrap/create_auth_db.sql" in workflow, (
f"В {_DEPLOY_WORKFLOW.name} нет bootstrap-шага создания БД auth."
)

View file

@ -40,8 +40,7 @@ _DB_OK, _DB_ERR = _db_reachable()
pytestmark = pytest.mark.skipif(
not _DB_OK,
reason=(
"Нет доступной postgres БД (TEST_DATABASE_URL/DATABASE_URL) — "
f"тест #99 пропущен: {_DB_ERR}"
f"Нет доступной postgres БД (TEST_DATABASE_URL/DATABASE_URL) — тест #99 пропущен: {_DB_ERR}"
),
)
@ -107,10 +106,7 @@ def conn():
def _insert_quarter(cur, q_start, bucket_area, price_m2, n) -> None:
"""Insert n single-flat ДДУ rows at given per-unit area + price/m²."""
rows = [
("002001003000", "ДДУ", 66, q_start, bucket_area, 1, price_m2)
for _ in range(n)
]
rows = [("002001003000", "ДДУ", 66, q_start, bucket_area, 1, price_m2) for _ in range(n)]
cur.executemany(
"INSERT INTO rd (realestate_type_code, doc_type, region_code, "
"period_start_date, area, deal_count, price_per_sqm) "
@ -136,7 +132,7 @@ def _setup(cur: psycopg.Cursor) -> None:
# Bucket 4 (60-80 m²): 2025-Q3 present, 2025-Q4 SPARSE (<10 → filtered),
# 2026-Q1 present. index_previous for 2026-Q1 must compare to 2025-Q3.
_insert_quarter(cur, "2025-07-01", 70, 150000, 11)
_insert_quarter(cur, "2025-10-01", 70, 999999, 3) # below min_deals → dropped
_insert_quarter(cur, "2025-10-01", 70, 999999, 3) # below min_deals → dropped
_insert_quarter(cur, "2026-01-01", 70, 165000, 11)
# Packaged-deal trap: one row area=350 deal_count=7 → per-unit 50 m² (bucket 3),
# NOT bucket 6. Price chosen mid-range so it doesn't move the median much.

View file

@ -39,8 +39,7 @@ _DB_OK, _DB_ERR = _db_reachable()
pytestmark = pytest.mark.skipif(
not _DB_OK,
reason=(
"Нет доступной postgres БД (TEST_DATABASE_URL/DATABASE_URL) — "
f"тест #17 пропущен: {_DB_ERR}"
f"Нет доступной postgres БД (TEST_DATABASE_URL/DATABASE_URL) — тест #17 пропущен: {_DB_ERR}"
),
)
@ -107,8 +106,15 @@ def _setup(cur: psycopg.Cursor) -> None:
)
snap = "2026-04-28" # stale scrape date; data months end 2025-12 (4-mo gap)
months = [
"2025-04-01", "2025-05-01", "2025-06-01", "2025-07-01", "2025-08-01",
"2025-09-01", "2025-10-01", "2025-11-01", "2025-12-01",
"2025-04-01",
"2025-05-01",
"2025-06-01",
"2025-07-01",
"2025-08-01",
"2025-09-01",
"2025-10-01",
"2025-11-01",
"2025-12-01",
]
# obj 1 — sharp drop: prior ~15/mo, recent ~4/mo -> alert
dropper = [16, 14, 15, 17, 13, 14, 5, 4, 3]

View file

@ -153,9 +153,9 @@ def test_session_usable_after_close_for_thumb_update(
resp = photos.get_photo(db=_session_with_photo_row, obj_id=1, file_id="f1", size="thumb")
assert (
seen.get("in_transaction") is False
), "миниатюра генерируется при открытой транзакции — соединение пула занято"
assert seen.get("in_transaction") is False, (
"миниатюра генерируется при открытой транзакции — соединение пула занято"
)
assert getattr(resp, "path", None) == str(generated)
# Главное: запись ПОСЛЕ close() действительно доехала до БД — читаем ОТДЕЛЬНЫМ

View file

@ -74,9 +74,9 @@ def test_row_assembly_keeps_none_not_zero() -> None:
src = inspect.getsource(m)
assert 'if r["avg_area_m2"] is not None else None' in src, "в сборке ряда None → 0.0"
assert (
'round(row["avg_area_m2"], 1) if row["avg_area_m2"] is not None else None' in src
), "round(row['avg_area_m2']) без проверки на None"
assert 'round(row["avg_area_m2"], 1) if row["avg_area_m2"] is not None else None' in src, (
"round(row['avg_area_m2']) без проверки на None"
)
def test_mix_weighted_area_excludes_rows_without_area() -> None:
@ -89,9 +89,9 @@ def test_mix_weighted_area_excludes_rows_without_area() -> None:
src = inspect.getsource(m)
assert "rb_area_total_deals" in src, "нет отдельного знаменателя для площади"
assert re.search(
r"rb_area_weighted\[rb\]\s*/\s*rb_area_total_deals\[rb\]", src
), "площадь по-прежнему делится на все сделки (rb_deals), а не на ряды с площадью"
assert re.search(r"rb_area_weighted\[rb\]\s*/\s*rb_area_total_deals\[rb\]", src), (
"площадь по-прежнему делится на все сделки (rb_deals), а не на ряды с площадью"
)
def test_pdf_renders_dash_for_missing_area() -> None:

View file

@ -87,9 +87,10 @@ def test_classify_path_forecast() -> None:
def test_classify_path_forecast_export_not_confused_with_forecast() -> None:
"""forecast/export должен дать action='export', НЕ 'forecast'."""
assert audit_mod.classify_path(
"/api/v1/parcels/66:41:0204016:10/forecast/export"
) == ("export", "66:41:0204016:10")
assert audit_mod.classify_path("/api/v1/parcels/66:41:0204016:10/forecast/export") == (
"export",
"66:41:0204016:10",
)
def test_classify_path_unmatched_returns_none() -> None:
@ -134,9 +135,10 @@ def test_classify_path_insight_nested_path_not_matched() -> None:
def test_classify_path_parcels_method_ignored() -> None:
"""Parcels-паттерны не зависят от method — analyze матчится при любом методе."""
assert audit_mod.classify_path(
"/api/v1/parcels/66:41:0204016:10/analyze", "GET"
) == ("analyze", "66:41:0204016:10")
assert audit_mod.classify_path("/api/v1/parcels/66:41:0204016:10/analyze", "GET") == (
"analyze",
"66:41:0204016:10",
)
# ---------------------------------------------------------------------------

View file

@ -167,8 +167,7 @@ def test_parse_no_table_returns_empty() -> None:
def test_parse_no_header_row_returns_empty() -> None:
"""Нет строки-заголовка (маркеры «наименование»+«проектн») → пусто (defensive)."""
html = (
"<table><tr><th>Колонка А</th><th>Колонка Б</th></tr>"
"<tr><td>1</td><td>2</td></tr></table>"
"<table><tr><th>Колонка А</th><th>Колонка Б</th></tr><tr><td>1</td><td>2</td></tr></table>"
)
assert gg.parse_grs_table(html) == []

View file

@ -129,9 +129,7 @@ def test_residential_main_vri_zh5_with_mkd_true():
def test_residential_main_vri_overrides_subcategory():
"""main_vri ИЖС перебивает даже raw_props subcategory=2 (authoritative wins)."""
assert (
is_residential_zone("Ж-2", None, {"subcategory": 2}, main_vri=_VRI_IZHS_ONLY) is False
)
assert is_residential_zone("Ж-2", None, {"subcategory": 2}, main_vri=_VRI_IZHS_ONLY) is False
def test_residential_empty_main_vri_falls_back_to_regex():

View file

@ -28,9 +28,9 @@ def test_category_weight_metro():
metro_w = _category_weight("metro_stop")
for cat in CATEGORY_WEIGHTS:
if cat != "metro_stop" and cat != "default":
assert metro_w >= _category_weight(
cat
), f"metro_stop weight {metro_w} должен быть >= {cat} weight {_category_weight(cat)}"
assert metro_w >= _category_weight(cat), (
f"metro_stop weight {metro_w} должен быть >= {cat} weight {_category_weight(cat)}"
)
def test_category_weight_unknown_returns_default():
@ -130,9 +130,9 @@ def test_metro_beats_school_at_equal_distance():
]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
assert (
result.top_poi[0].category == "metro_stop"
), "При равном расстоянии метро (category_weight=6.0) должно быть выше школы (5.0)"
assert result.top_poi[0].category == "metro_stop", (
"При равном расстоянии метро (category_weight=6.0) должно быть выше школы (5.0)"
)
def test_metro_first_when_close():
@ -145,7 +145,7 @@ def test_metro_first_when_close():
result = compute_poi_weighted_top7(db, "66:41:0204016:10", 56.838, 60.605)
assert result.top_poi[0].category == "metro_stop", (
"Метро (weight=6.0) в 50м должно быть впереди школы (weight=5.0) в 300м — "
f"metro_weight={(1/(50+100))*6:.5f} vs school_weight={(1/(300+100))*5:.5f}"
f"metro_weight={(1 / (50 + 100)) * 6:.5f} vs school_weight={(1 / (300 + 100)) * 5:.5f}"
)
@ -194,9 +194,9 @@ def test_score_contribution_in_range():
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "cad", 56.838, 60.605)
for item in result.top_poi:
assert (
0.0 <= item.score_contribution <= 100.0
), f"{item.category} score_contribution={item.score_contribution} вне 0..100"
assert 0.0 <= item.score_contribution <= 100.0, (
f"{item.category} score_contribution={item.score_contribution} вне 0..100"
)
def test_metro_at_zero_distance_scores_high():
@ -205,9 +205,9 @@ def test_metro_at_zero_distance_scores_high():
rows = [_make_row("Метро у дома", "metro_stop", 0.0)]
db = _MockDb(rows)
result = compute_poi_weighted_top7(db, "cad", 56.838, 60.605)
assert (
result.poi_weighted_score >= 19.0
), f"Метро у дома (d=0) должно давать ≥19/100, получили {result.poi_weighted_score}"
assert result.poi_weighted_score >= 19.0, (
f"Метро у дома (d=0) должно давать ≥19/100, получили {result.poi_weighted_score}"
)
def test_score_contribution_sum_equals_total():

View file

@ -399,7 +399,7 @@ def _make_zouit_row(
def test_cad_zouit_overlaps_includes_geom_geojson() -> None:
"""#255: каждый overlap содержит geom_geojson (string из ST_AsGeoJSON)."""
geojson_str = (
'{"type":"Polygon","coordinates":' "[[[60.6,56.8],[60.7,56.8],[60.7,56.9],[60.6,56.8]]]}"
'{"type":"Polygon","coordinates":[[[60.6,56.8],[60.7,56.8],[60.7,56.9],[60.6,56.8]]]}'
)
rows = [
_make_zouit_row(
@ -568,9 +568,9 @@ def test_empty_result_reports_unknown_risk_coverage() -> None:
"""
result = make_empty_result()
assert (
"risks_count" in result["nspd_dump"]
), "признак покрытия не отдаётся — фронт не сможет отличить «чисто» от «не спрашивали»"
assert "risks_count" in result["nspd_dump"], (
"признак покрытия не отдаётся — фронт не сможет отличить «чисто» от «не спрашивали»"
)
assert result["nspd_dump"]["risks_count"] is None

View file

@ -476,9 +476,9 @@ def test_local_variables_never_reach_transport(module: str) -> None:
assert probe["counts"]["exception"] == 1
assert "sentry-wiring-probe boom" in payload, "событие с исключением не доехало"
assert (
probe["markers"]["local_var"] not in payload
), f"{module}: значение локальной переменной ушло в мониторинг"
assert probe["markers"]["local_var"] not in payload, (
f"{module}: значение локальной переменной ушло в мониторинг"
)
@pytest.mark.parametrize("module", ["app.main", "app.workers.celery_app"])
@ -493,7 +493,7 @@ def test_scrub_failure_does_not_spawn_second_event(module: str) -> None:
"""
probe = json.loads(_probe(module))
assert (
probe["scrub_handler_entries"] == 1
), "сбой скраба вернулся вторым событием: строка журнала уходит в мониторинг"
assert probe["scrub_handler_entries"] == 1, (
"сбой скраба вернулся вторым событием: строка журнала уходит в мониторинг"
)
assert probe["counts"]["scrub_failure"] == 1

View file

@ -27,9 +27,9 @@ def test_beat_schedule_includes_gas_outlet_sync_weekly_tuesday() -> None:
with patch.object(beat_schedule, "_build_beat_schedule_from_db", return_value={}):
schedule = beat_schedule.build_beat_schedule()
assert (
"gas-outlet-points-sync-weekly" in schedule
), f"gas-outlet-points-sync-weekly отсутствует в beat: {sorted(schedule.keys())}"
assert "gas-outlet-points-sync-weekly" in schedule, (
f"gas-outlet-points-sync-weekly отсутствует в beat: {sorted(schedule.keys())}"
)
entry = schedule["gas-outlet-points-sync-weekly"]
assert entry["task"] == "tasks.connection_capacity_sync.sync_gas_outlet_points"
# crontab day_of_week = вторник (2).

View file

@ -86,9 +86,9 @@ def test_extract_coords_inverted_would_fail_bbox() -> None:
inverted_lon = feature["geometry"]["coordinates"][0] # 56.83 → не в bbox [5863]
inverted_lat = feature["geometry"]["coordinates"][1] # 60.61 → не в bbox [5558.5]
inverted_inside = (55.0 <= inverted_lat <= 58.5) and (58.0 <= inverted_lon <= 63.0)
assert (
not inverted_inside
), "Перепутанные координаты прошли bbox-фильтр — _extract_coords не обнаружит баг coord swap"
assert not inverted_inside, (
"Перепутанные координаты прошли bbox-фильтр — _extract_coords не обнаружит баг coord swap"
)
# Проверяем что реальный _extract_coords возвращает КОРРЕКТНЫЕ (lon, lat)
result = _extract_coords(feature)
@ -141,9 +141,9 @@ def test_address_passes_include_ekb_districts() -> None:
"Верх-Исетский",
"Железнодорожный",
}
assert expected_districts <= set(
_ADDRESS_PASSES
), f"Не все районы ЕКБ в _ADDRESS_PASSES: missing={expected_districts - set(_ADDRESS_PASSES)}"
assert expected_districts <= set(_ADDRESS_PASSES), (
f"Не все районы ЕКБ в _ADDRESS_PASSES: missing={expected_districts - set(_ADDRESS_PASSES)}"
)
def test_address_passes_order_city_before_district() -> None:

View file

@ -119,17 +119,17 @@ def test_finished_at_is_last_sign_of_life_not_now() -> None:
"""
sql = _objective_update(_run())
assert sql is not None
assert (
"COALESCE(heartbeat_at, started_at)" in sql
), f"finished_at ставится не по последнему признаку жизни:\n{sql}"
assert (
"finished_at = NOW()" not in sql
), f"finished_at = NOW() — время завершения соврано:\n{sql}"
assert "COALESCE(heartbeat_at, started_at)" in sql, (
f"finished_at ставится не по последнему признаку жизни:\n{sql}"
)
assert "finished_at = NOW()" not in sql, (
f"finished_at = NOW() — время завершения соврано:\n{sql}"
)
def test_kn_sweep_still_runs() -> None:
"""Контроль от регресса: добавление Объектива не сломало подметание kn."""
db = _run()
assert any(
"kn_scrape_runs" in s for s in db.sql
), f"подметание kn_scrape_runs пропало; выполнено: {db.sql}"
assert any("kn_scrape_runs" in s for s in db.sql), (
f"подметание kn_scrape_runs пропало; выполнено: {db.sql}"
)

View file

@ -569,9 +569,9 @@ def test_soft_time_limit_exceeded_flushes_heartbeat(monkeypatch: Any) -> None:
for sql, params in captured
if "heartbeat_at = NOW()" in sql and "targets_done" in sql
]
assert (
heartbeat_updates
), "SoftTimeLimitExceeded handler должен flush'нуть heartbeat с counters перед raise"
assert heartbeat_updates, (
"SoftTimeLimitExceeded handler должен flush'нуть heartbeat с counters перед raise"
)
def test_soft_time_limit_exceeded_does_not_overwrite_paused_with_failed(

View file

@ -24,8 +24,11 @@ def _source(
def _payload(sources: list[dict[str, Any]], overall: str) -> dict[str, Any]:
return {"generated_at": "2026-06-25T09:00:00+00:00", "overall_status": overall,
"sources": sources}
return {
"generated_at": "2026-06-25T09:00:00+00:00",
"overall_status": overall,
"sources": sources,
}
def _run_with(payload_or_exc: Any) -> tuple[dict[str, Any], MagicMock]:

View file

@ -47,7 +47,9 @@ async def search(
if cached is not None:
logger.info(
"search cache HIT key=%s page=%d size=%d",
cache_key[:24], params.page, params.page_size,
cache_key[:24],
params.page,
params.page_size,
)
resp = SearchResponse.model_validate(cached)
resp.cache_hit = True
@ -73,6 +75,10 @@ async def search(
await cache.set(cache_key, response.model_dump(mode="json"), ttl=cache.TTL_SEARCH)
logger.info(
"search MISS page=%d size=%d total=%d items=%d elapsed=%.1fms",
params.page, params.page_size, total, len(items), elapsed_ms,
params.page,
params.page_size,
total,
len(items),
elapsed_ms,
)
return response

View file

@ -48,9 +48,7 @@ class SearchCache:
async def set(self, key: str, value: dict[str, Any], ttl: int) -> None:
try:
await self._client.set(
key, json.dumps(value, default=str, ensure_ascii=False), ex=ttl
)
await self._client.set(key, json.dumps(value, default=str, ensure_ascii=False), ex=ttl)
except Exception as e:
logger.warning("redis SET failed key=%s: %s", key[:24], e)

View file

@ -1091,7 +1091,7 @@ def _save_yandex_history_items(
if skipped_area > 0:
logger.info(
"yandex_valuation: skipped %d/%d history items with area_m2 <= 0 or None" " (addr=%r)",
"yandex_valuation: skipped %d/%d history items with area_m2 <= 0 or None (addr=%r)",
skipped_area,
len(result.history_items),
result.address,
@ -3328,8 +3328,7 @@ def _price_from_inputs(
)
if same_quarter_ratio > settings.estimate_quarter_match_skip_ratio:
logger.info(
"quarter_index: Guard-2 skip (same-quarter ratio=%.2f > %.2f)"
" for %s",
"quarter_index: Guard-2 skip (same-quarter ratio=%.2f > %.2f) for %s",
same_quarter_ratio,
settings.estimate_quarter_match_skip_ratio,
target_quarter,
@ -6980,7 +6979,7 @@ def _enforce_zero_analog_low(
"""
if n_analogs == 0 and confidence != "low":
logger.warning(
"ghost_anchor_guard #1871: forcing confidence 'low' (was %s, " "median=%s, sources=%s)",
"ghost_anchor_guard #1871: forcing confidence 'low' (was %s, median=%s, sources=%s)",
confidence,
median_price,
sources_used,

View file

@ -1665,9 +1665,12 @@ def _build_deals_page(estimate: AggregatedEstimate, input_snapshot: dict, brand)
<table style="width:100%;border-collapse:collapse;">
<tr><td style="padding:4pt 0;">Количество сделок по аналогичном объектам</td>
<td class="bold" style="text-align:right;">{_mono(f"{n_deals} шт.")}</td></tr>
{f'''<tr><td style="padding:4pt 0;">Сделки</td>
{
f'''<tr><td style="padding:4pt 0;">Сделки</td>
<td class="bold" style="text-align:right;">{_mono(deals_as_of)}</td></tr>'''
if deals_as_of else ""}
if deals_as_of
else ""
}
</table>
<div style="margin-top:14pt;font-size:{_FS_SM};color:{_MUTED};">
<span class="bullet-dot" style="margin-right:5pt;"></span>Источники данных</div>

Some files were not shown because too many files have changed in this diff Show more