diff --git a/backend/app/schemas/own_project.py b/backend/app/schemas/own_project.py index 3d878d7e..e3e2c661 100644 --- a/backend/app/schemas/own_project.py +++ b/backend/app/schemas/own_project.py @@ -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)" ) diff --git a/backend/app/services/analytics/ddu_price_indicator.py b/backend/app/services/analytics/ddu_price_indicator.py index cf7268e8..df503d0e 100644 --- a/backend/app/services/analytics/ddu_price_indicator.py +++ b/backend/app/services/analytics/ddu_price_indicator.py @@ -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() diff --git a/backend/app/services/analytics_queries.py b/backend/app/services/analytics_queries.py index 9e6ef0ab..8393b48e 100644 --- a/backend/app/services/analytics_queries.py +++ b/backend/app/services/analytics_queries.py @@ -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: diff --git a/backend/app/services/chat/intents.py b/backend/app/services/chat/intents.py index d821c7e9..7d66c009 100644 --- a/backend/app/services/chat/intents.py +++ b/backend/app/services/chat/intents.py @@ -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 diff --git a/backend/app/services/etl/newbuilding_crossload.py b/backend/app/services/etl/newbuilding_crossload.py index ab5ed376..ad5ef66d 100644 --- a/backend/app/services/etl/newbuilding_crossload.py +++ b/backend/app/services/etl/newbuilding_crossload.py @@ -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, diff --git a/backend/app/services/exporters/report_docx.py b/backend/app/services/exporters/report_docx.py index 96663941..6c47930f 100644 --- a/backend/app/services/exporters/report_docx.py +++ b/backend/app/services/exporters/report_docx.py @@ -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 = [ diff --git a/backend/app/services/exporters/report_maps.py b/backend/app/services/exporters/report_maps.py index 44fe62db..3f9e3cbd 100644 --- a/backend/app/services/exporters/report_maps.py +++ b/backend/app/services/exporters/report_maps.py @@ -85,8 +85,7 @@ _CONCEPT_FOOTPRINT_STYLE = { } _MAP_UNAVAILABLE_HTML = ( - '
Карта недоступна — геоданные участка отсутствуют ' - "в отчёте
" + '
Карта недоступна — геоданные участка отсутствуют в отчёте
' ) diff --git a/backend/app/services/forecasting/affordability.py b/backend/app/services/forecasting/affordability.py index 04235f7d..ac865265 100644 --- a/backend/app/services/forecasting/affordability.py +++ b/backend/app/services/forecasting/affordability.py @@ -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: diff --git a/backend/app/services/generative/catalog.py b/backend/app/services/generative/catalog.py index 2ed0c21c..45f0fb93 100644 --- a/backend/app/services/generative/catalog.py +++ b/backend/app/services/generative/catalog.py @@ -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 diff --git a/backend/app/services/generative/geometry.py b/backend/app/services/generative/geometry.py index 930ce619..c4052623 100644 --- a/backend/app/services/generative/geometry.py +++ b/backend/app/services/generative/geometry.py @@ -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) diff --git a/backend/app/services/llm/client.py b/backend/app/services/llm/client.py index 772db0b3..c27fdec8 100644 --- a/backend/app/services/llm/client.py +++ b/backend/app/services/llm/client.py @@ -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)", diff --git a/backend/app/services/site_finder/eias_heat_loader.py b/backend/app/services/site_finder/eias_heat_loader.py index 6b340fa9..02febce0 100644 --- a/backend/app/services/site_finder/eias_heat_loader.py +++ b/backend/app/services/site_finder/eias_heat_loader.py @@ -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: diff --git a/backend/app/services/site_finder/gate_verdict.py b/backend/app/services/site_finder/gate_verdict.py index 4b3a8726..bff1dd4c 100644 --- a/backend/app/services/site_finder/gate_verdict.py +++ b/backend/app/services/site_finder/gate_verdict.py @@ -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', '')}" ), ) ) diff --git a/backend/app/services/site_finder/market_metrics.py b/backend/app/services/site_finder/market_metrics.py index d6347644..8ead9006 100644 --- a/backend/app/services/site_finder/market_metrics.py +++ b/backend/app/services/site_finder/market_metrics.py @@ -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, diff --git a/backend/app/services/site_finder/noise_loader.py b/backend/app/services/site_finder/noise_loader.py index 32755c83..627997a4 100644 --- a/backend/app/services/site_finder/noise_loader.py +++ b/backend/app/services/site_finder/noise_loader.py @@ -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]: diff --git a/backend/app/services/site_finder/pzz_loader.py b/backend/app/services/site_finder/pzz_loader.py index 7d3c3a12..87e25c29 100644 --- a/backend/app/services/site_finder/pzz_loader.py +++ b/backend/app/services/site_finder/pzz_loader.py @@ -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) diff --git a/backend/app/workers/tasks/scrape_objective.py b/backend/app/workers/tasks/scrape_objective.py index 7ab5506d..76270c40 100644 --- a/backend/app/workers/tasks/scrape_objective.py +++ b/backend/app/workers/tasks/scrape_objective.py @@ -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, diff --git a/backend/scripts/spike_plan_vectorize.py b/backend/scripts/spike_plan_vectorize.py index 2ffd2a77..1f4b3cee 100644 --- a/backend/scripts/spike_plan_vectorize.py +++ b/backend/scripts/spike_plan_vectorize.py @@ -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: diff --git a/backend/tests/api/v1/test_2464_confidence_zoning_source.py b/backend/tests/api/v1/test_2464_confidence_zoning_source.py index 6878d6e4..71cd130a 100644 --- a/backend/tests/api/v1/test_2464_confidence_zoning_source.py +++ b/backend/tests/api/v1/test_2464_confidence_zoning_source.py @@ -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 diff --git a/backend/tests/api/v1/test_2464g_noise_source_filter.py b/backend/tests/api/v1/test_2464g_noise_source_filter.py index af948cdc..cfd60e80 100644 --- a/backend/tests/api/v1/test_2464g_noise_source_filter.py +++ b/backend/tests/api/v1/test_2464g_noise_source_filter.py @@ -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: diff --git a/backend/tests/api/v1/test_2934_no_fake_geology_label.py b/backend/tests/api/v1/test_2934_no_fake_geology_label.py index 96f49925..17ccd476 100644 --- a/backend/tests/api/v1/test_2934_no_fake_geology_label.py +++ b/backend/tests/api/v1/test_2934_no_fake_geology_label.py @@ -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: diff --git a/backend/tests/api/v1/test_analyze_competitors_status.py b/backend/tests/api/v1/test_analyze_competitors_status.py index 055a2d34..7174b6dd 100644 --- a/backend/tests/api/v1/test_analyze_competitors_status.py +++ b/backend/tests/api/v1/test_analyze_competitors_status.py @@ -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: diff --git a/backend/tests/api/v1/test_analyze_inline_weights.py b/backend/tests/api/v1/test_analyze_inline_weights.py index cc61f0ef..35ffb40e 100644 --- a/backend/tests/api/v1/test_analyze_inline_weights.py +++ b/backend/tests/api/v1/test_analyze_inline_weights.py @@ -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() diff --git a/backend/tests/api/v1/test_insights.py b/backend/tests/api/v1/test_insights.py index ad202afd..c84cac60 100644 --- a/backend/tests/api/v1/test_insights.py +++ b/backend/tests/api/v1/test_insights.py @@ -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 diff --git a/backend/tests/api/v1/test_market_pulse_and_neighbors_honesty.py b/backend/tests/api/v1/test_market_pulse_and_neighbors_honesty.py index f435dc5a..ae211c57 100644 --- a/backend/tests/api/v1/test_market_pulse_and_neighbors_honesty.py +++ b/backend/tests/api/v1/test_market_pulse_and_neighbors_honesty.py @@ -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: diff --git a/backend/tests/api/v1/test_own_projects.py b/backend/tests/api/v1/test_own_projects.py index 06e67314..1a791839 100644 --- a/backend/tests/api/v1/test_own_projects.py +++ b/backend/tests/api/v1/test_own_projects.py @@ -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"] == "Переименовано" diff --git a/backend/tests/api/v1/test_parcel_competitors.py b/backend/tests/api/v1/test_parcel_competitors.py index d49138ab..1e3c0bfd 100644 --- a/backend/tests/api/v1/test_parcel_competitors.py +++ b/backend/tests/api/v1/test_parcel_competitors.py @@ -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 маппинге" diff --git a/backend/tests/integration/test_analyze_parcels_sql.py b/backend/tests/integration/test_analyze_parcels_sql.py index b50d0577..49dde2cc 100644 --- a/backend/tests/integration/test_analyze_parcels_sql.py +++ b/backend/tests/integration/test_analyze_parcels_sql.py @@ -104,9 +104,9 @@ class TestNeighborsSummarySql: for kw in forbidden_aliases: # ищем паттерн ``WITH AS (`` или ``, 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 ────────────────────────────────────────────────── diff --git a/backend/tests/ops/test_2950_deploy_concurrency_group.py b/backend/tests/ops/test_2950_deploy_concurrency_group.py index b8fa59b2..a2e8b519 100644 --- a/backend/tests/ops/test_2950_deploy_concurrency_group.py +++ b/backend/tests/ops/test_2950_deploy_concurrency_group.py @@ -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 — отменённый деплой оставляет " "прод на старом коде ровно так же, как упавший" diff --git a/backend/tests/scrapers/test_cbr_macro.py b/backend/tests/scrapers/test_cbr_macro.py index 5b03f0c3..ffffc1e3 100644 --- a/backend/tests/scrapers/test_cbr_macro.py +++ b/backend/tests/scrapers/test_cbr_macro.py @@ -41,7 +41,7 @@ FIXTURE_KEYRATE_XML = ( # где угодно), несмотря на schema-блок и namespace на KR. FIXTURE_KEYRATE_DIFFGRAM = ( '' - "" + '' '' '' diff --git a/backend/tests/scrapers/test_emiss_sdmx.py b/backend/tests/scrapers/test_emiss_sdmx.py index 8b356e1d..6cba33ba 100644 --- a/backend/tests/scrapers/test_emiss_sdmx.py +++ b/backend/tests/scrapers/test_emiss_sdmx.py @@ -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: diff --git a/backend/tests/scrapers/test_nspd_bulk_client.py b/backend/tests/scrapers/test_nspd_bulk_client.py index bc57857a..ef93c852 100644 --- a/backend/tests/scrapers/test_nspd_bulk_client.py +++ b/backend/tests/scrapers/test_nspd_bulk_client.py @@ -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)}" + ) diff --git a/backend/tests/scrapers/test_nspd_grid_walk.py b/backend/tests/scrapers/test_nspd_grid_walk.py index 26d8e625..a34bceac 100644 --- a/backend/tests/scrapers/test_nspd_grid_walk.py +++ b/backend/tests/scrapers/test_nspd_grid_walk.py @@ -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" diff --git a/backend/tests/scrapers/test_rosstat_emiss.py b/backend/tests/scrapers/test_rosstat_emiss.py index bb3c2c30..525a2c13 100644 --- a/backend/tests/scrapers/test_rosstat_emiss.py +++ b/backend/tests/scrapers/test_rosstat_emiss.py @@ -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" diff --git a/backend/tests/services/analysis_runs/test_repository_run_history.py b/backend/tests/services/analysis_runs/test_repository_run_history.py index f5201e01..fe54cb16 100644 --- a/backend/tests/services/analysis_runs/test_repository_run_history.py +++ b/backend/tests/services/analysis_runs/test_repository_run_history.py @@ -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 diff --git a/backend/tests/services/cadastre/test_2464_backfill_waf_aborts.py b/backend/tests/services/cadastre/test_2464_backfill_waf_aborts.py index e9d7a146..733ca4df 100644 --- a/backend/tests/services/cadastre/test_2464_backfill_waf_aborts.py +++ b/backend/tests/services/cadastre/test_2464_backfill_waf_aborts.py @@ -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}" diff --git a/backend/tests/services/exporters/test_2464_concept_pdf_sales_window.py b/backend/tests/services/exporters/test_2464_concept_pdf_sales_window.py index 7104d93a..711dec1b 100644 --- a/backend/tests/services/exporters/test_2464_concept_pdf_sales_window.py +++ b/backend/tests/services/exporters/test_2464_concept_pdf_sales_window.py @@ -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 diff --git a/backend/tests/services/exporters/test_2464_zouit_count_label.py b/backend/tests/services/exporters/test_2464_zouit_count_label.py index ec17ce91..f2c46189 100644 --- a/backend/tests/services/exporters/test_2464_zouit_count_label.py +++ b/backend/tests/services/exporters/test_2464_zouit_count_label.py @@ -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}: типы ЗОУИТ пропали из сводки" + ) diff --git a/backend/tests/services/exporters/test_2934_flood_row_honesty.py b/backend/tests/services/exporters/test_2934_flood_row_honesty.py index 03fda10c..3ae8b247 100644 --- a/backend/tests/services/exporters/test_2934_flood_row_honesty.py +++ b/backend/tests/services/exporters/test_2934_flood_row_honesty.py @@ -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 держит свою копию метки — форматы разъедутся при следующей правке" + ) diff --git a/backend/tests/services/exporters/test_excel.py b/backend/tests/services/exporters/test_excel.py index b639775f..549a075d 100644 --- a/backend/tests/services/exporters/test_excel.py +++ b/backend/tests/services/exporters/test_excel.py @@ -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: diff --git a/backend/tests/services/forecasting/test_confidence_engine.py b/backend/tests/services/forecasting/test_confidence_engine.py index b946e28f..572f0566 100644 --- a/backend/tests/services/forecasting/test_confidence_engine.py +++ b/backend/tests/services/forecasting/test_confidence_engine.py @@ -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) diff --git a/backend/tests/services/forecasting/test_normalize.py b/backend/tests/services/forecasting/test_normalize.py index f85229b3..be5cc9d7 100644 --- a/backend/tests/services/forecasting/test_normalize.py +++ b/backend/tests/services/forecasting/test_normalize.py @@ -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: diff --git a/backend/tests/services/forecasting/test_scenarios.py b/backend/tests/services/forecasting/test_scenarios.py index 94999a10..2588cb99 100644 --- a/backend/tests/services/forecasting/test_scenarios.py +++ b/backend/tests/services/forecasting/test_scenarios.py @@ -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 ] diff --git a/backend/tests/services/generative/test_market_price_lookup.py b/backend/tests/services/generative/test_market_price_lookup.py index 5667f38e..abdd5fe3 100644 --- a/backend/tests/services/generative/test_market_price_lookup.py +++ b/backend/tests/services/generative/test_market_price_lookup.py @@ -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: diff --git a/backend/tests/services/generative/test_teap_financial.py b/backend/tests/services/generative/test_teap_financial.py index a8757ff4..0909f1f9 100644 --- a/backend/tests/services/generative/test_teap_financial.py +++ b/backend/tests/services/generative/test_teap_financial.py @@ -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 diff --git a/backend/tests/services/llm/test_client.py b/backend/tests/services/llm/test_client.py index 838165a2..63c56f60 100644 --- a/backend/tests/services/llm/test_client.py +++ b/backend/tests/services/llm/test_client.py @@ -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 на часы. """ diff --git a/backend/tests/services/llm/test_provider.py b/backend/tests/services/llm/test_provider.py index eccf3434..18ba9f5b 100644 --- a/backend/tests/services/llm/test_provider.py +++ b/backend/tests/services/llm/test_provider.py @@ -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 diff --git a/backend/tests/services/scrapers/test_2464_catalog_waf_breaker.py b/backend/tests/services/scrapers/test_2464_catalog_waf_breaker.py index ad58d072..151be295 100644 --- a/backend/tests/services/scrapers/test_2464_catalog_waf_breaker.py +++ b/backend/tests/services/scrapers/test_2464_catalog_waf_breaker.py @@ -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 diff --git a/backend/tests/services/scrapers/test_2464_detect_kind_position.py b/backend/tests/services/scrapers/test_2464_detect_kind_position.py index 647ea28f..2fb124a1 100644 --- a/backend/tests/services/scrapers/test_2464_detect_kind_position.py +++ b/backend/tests/services/scrapers/test_2464_detect_kind_position.py @@ -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: diff --git a/backend/tests/services/scrapers/test_2464_status_negation.py b/backend/tests/services/scrapers/test_2464_status_negation.py index c2a6fbcb..d54591ce 100644 --- a/backend/tests/services/scrapers/test_2464_status_negation.py +++ b/backend/tests/services/scrapers/test_2464_status_negation.py @@ -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}" + ) diff --git a/backend/tests/services/scrapers/test_2464_tep_docstring_truth.py b/backend/tests/services/scrapers/test_2464_tep_docstring_truth.py index 1394c84b..fc809698 100644 --- a/backend/tests/services/scrapers/test_2464_tep_docstring_truth.py +++ b/backend/tests/services/scrapers/test_2464_tep_docstring_truth.py @@ -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 текст, "не назван живой портал ГИСОГД-СО" diff --git a/backend/tests/services/scrapers/test_domrf_catalog_object.py b/backend/tests/services/scrapers/test_domrf_catalog_object.py index 34012edd..30da2a2d 100644 --- a/backend/tests/services/scrapers/test_domrf_catalog_object.py +++ b/backend/tests/services/scrapers/test_domrf_catalog_object.py @@ -42,7 +42,7 @@ def test_extract_next_data_from_html() -> None: def test_extract_next_data_single_quotes() -> None: """Тег с одинарными кавычками тоже должен парситься.""" - html = "" + html = '' result = extract_next_data(html) assert "props" in result diff --git a/backend/tests/services/scrapers/test_domrf_catalog_parse.py b/backend/tests/services/scrapers/test_domrf_catalog_parse.py index a9214814..786aec07 100644 --- a/backend/tests/services/scrapers/test_domrf_catalog_parse.py +++ b/backend/tests/services/scrapers/test_domrf_catalog_parse.py @@ -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"{nav_text}" if nav_text else "" - return ( - "" - f"{nav}" - f"{label}" - f"{value}" - "" - ) + return f"{nav}{label}{value}" def _html_blocks_only(blocks: list[tuple[str, str]]) -> str: diff --git a/backend/tests/services/scrapers/test_domrf_flat_plan_url.py b/backend/tests/services/scrapers/test_domrf_flat_plan_url.py index ef613b7b..acdb2e0f 100644 --- a/backend/tests/services/scrapers/test_domrf_flat_plan_url.py +++ b/backend/tests/services/scrapers/test_domrf_flat_plan_url.py @@ -115,10 +115,7 @@ def test_plan_from_img_proximity_to_label() -> None: """ без plan-hint в атрибутах, но рядом с блоком «Планировка».""" plain = f"{BASE_URL}/api/ext/file/imgabc.png" html = ( - "" - "
Планировка
" - f'' - "" + f'
Планировка
' ) got = extract_plan_image_url(html, _collector_for(html)) assert got == plain diff --git a/backend/tests/services/scrapers/test_domrf_kn_upsert_sql.py b/backend/tests/services/scrapers/test_domrf_kn_upsert_sql.py index c0f7de00..f31b97bf 100644 --- a/backend/tests/services/scrapers/test_domrf_kn_upsert_sql.py +++ b/backend/tests/services/scrapers/test_domrf_kn_upsert_sql.py @@ -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 diff --git a/backend/tests/services/scrapers/test_okn_egrkn_client.py b/backend/tests/services/scrapers/test_okn_egrkn_client.py index 6d1740c6..fb7a6d87 100644 --- a/backend/tests/services/scrapers/test_okn_egrkn_client.py +++ b/backend/tests/services/scrapers/test_okn_egrkn_client.py @@ -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 формально в диапазоне 55–58.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: diff --git a/backend/tests/services/site_finder/test_2464_capacity_savepoints.py b/backend/tests/services/site_finder/test_2464_capacity_savepoints.py index c43e1d77..760d448e 100644 --- a/backend/tests/services/site_finder/test_2464_capacity_savepoints.py +++ b/backend/tests/services/site_finder/test_2464_capacity_savepoints.py @@ -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}: запрос вообще не выполнялся" diff --git a/backend/tests/services/site_finder/test_2464_default_profile_order.py b/backend/tests/services/site_finder/test_2464_default_profile_order.py index b01e3352..6d23bb7a 100644 --- a/backend/tests/services/site_finder/test_2464_default_profile_order.py +++ b/backend/tests/services/site_finder/test_2464_default_profile_order.py @@ -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}" + ) diff --git a/backend/tests/services/site_finder/test_2464_heat_loader_tx.py b/backend/tests/services/site_finder/test_2464_heat_loader_tx.py index 304e6218..cb4cfad1 100644 --- a/backend/tests/services/site_finder/test_2464_heat_loader_tx.py +++ b/backend/tests/services/site_finder/test_2464_heat_loader_tx.py @@ -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, "чужая сессия закрыта — её закрывает вызывающий" diff --git a/backend/tests/services/site_finder/test_2464_teap_input_sanity.py b/backend/tests/services/site_finder/test_2464_teap_input_sanity.py index 16a86ff1..e885f4a8 100644 --- a/backend/tests/services/site_finder/test_2464_teap_input_sanity.py +++ b/backend/tests/services/site_finder/test_2464_teap_input_sanity.py @@ -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 не должна меняться от ограничения пятна" + ) diff --git a/backend/tests/services/site_finder/test_2464_zouit_label_all_kinds.py b/backend/tests/services/site_finder/test_2464_zouit_label_all_kinds.py index 31c08f46..18f17a18 100644 --- a/backend/tests/services/site_finder/test_2464_zouit_label_all_kinds.py +++ b/backend/tests/services/site_finder/test_2464_zouit_label_all_kinds.py @@ -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: diff --git a/backend/tests/services/site_finder/test_best_layouts.py b/backend/tests/services/site_finder/test_best_layouts.py index 8825c9c5..6ddc3906 100644 --- a/backend/tests/services/site_finder/test_best_layouts.py +++ b/backend/tests/services/site_finder/test_best_layouts.py @@ -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: diff --git a/backend/tests/services/site_finder/test_competitors_parking.py b/backend/tests/services/site_finder/test_competitors_parking.py index 58fb795d..fecc42eb 100644 --- a/backend/tests/services/site_finder/test_competitors_parking.py +++ b/backend/tests/services/site_finder/test_competitors_parking.py @@ -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", diff --git a/backend/tests/services/site_finder/test_pat_subzones.py b/backend/tests/services/site_finder/test_pat_subzones.py index 826f8ef0..61b79e38 100644 --- a/backend/tests/services/site_finder/test_pat_subzones.py +++ b/backend/tests/services/site_finder/test_pat_subzones.py @@ -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: diff --git a/backend/tests/services/site_finder/test_permits_nearby.py b/backend/tests/services/site_finder/test_permits_nearby.py index 36f82ae5..f39f8832 100644 --- a/backend/tests/services/site_finder/test_permits_nearby.py +++ b/backend/tests/services/site_finder/test_permits_nearby.py @@ -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: diff --git a/backend/tests/services/site_finder/test_supply_layers.py b/backend/tests/services/site_finder/test_supply_layers.py index 0e11e83f..7a10105f 100644 --- a/backend/tests/services/site_finder/test_supply_layers.py +++ b/backend/tests/services/site_finder/test_supply_layers.py @@ -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, diff --git a/backend/tests/services/test_2464_docstring_matches_code.py b/backend/tests/services/test_2464_docstring_matches_code.py index 136bc1f5..b14e07cc 100644 --- a/backend/tests/services/test_2464_docstring_matches_code.py +++ b/backend/tests/services/test_2464_docstring_matches_code.py @@ -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 не возвращается независимо от давности" + ) diff --git a/backend/tests/services/test_2464_three_small_leaks.py b/backend/tests/services/test_2464_three_small_leaks.py index 57479248..a3a5279f 100644 --- a/backend/tests/services/test_2464_three_small_leaks.py +++ b/backend/tests/services/test_2464_three_small_leaks.py @@ -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"]) diff --git a/backend/tests/services/test_2464a_job_settings_savepoint.py b/backend/tests/services/test_2464a_job_settings_savepoint.py index 5539f82c..c842536f 100644 --- a/backend/tests/services/test_2464a_job_settings_savepoint.py +++ b/backend/tests/services/test_2464a_job_settings_savepoint.py @@ -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: diff --git a/backend/tests/services/test_analytics_queries_domrf_dedup.py b/backend/tests/services/test_analytics_queries_domrf_dedup.py index a6db1805..77e0edf1 100644 --- a/backend/tests/services/test_analytics_queries_domrf_dedup.py +++ b/backend/tests/services/test_analytics_queries_domrf_dedup.py @@ -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 diff --git a/backend/tests/services/test_ekburg_permits.py b/backend/tests/services/test_ekburg_permits.py index 51c47cd2..1bd8f8b9 100644 --- a/backend/tests/services/test_ekburg_permits.py +++ b/backend/tests/services/test_ekburg_permits.py @@ -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: """Пр. Космонавтов — попадает в ЕКБ-регион.""" diff --git a/backend/tests/services/test_newbuilding_crossload.py b/backend/tests/services/test_newbuilding_crossload.py index 232cc7f1..fecd061d 100644 --- a/backend/tests/services/test_newbuilding_crossload.py +++ b/backend/tests/services/test_newbuilding_crossload.py @@ -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'ом" + ) diff --git a/backend/tests/services/test_recommend_mix_velocity.py b/backend/tests/services/test_recommend_mix_velocity.py index 73ea6e0a..38804a2c 100644 --- a/backend/tests/services/test_recommend_mix_velocity.py +++ b/backend/tests/services/test_recommend_mix_velocity.py @@ -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: diff --git a/backend/tests/services/test_weather_cache.py b/backend/tests/services/test_weather_cache.py index ed5b5f71..8e3f6a7c 100644 --- a/backend/tests/services/test_weather_cache.py +++ b/backend/tests/services/test_weather_cache.py @@ -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.""" diff --git a/backend/tests/sql/test_2464_act_date_backfill.py b/backend/tests/sql/test_2464_act_date_backfill.py index bc6a0311..9442753f 100644 --- a/backend/tests/sql/test_2464_act_date_backfill.py +++ b/backend/tests/sql/test_2464_act_date_backfill.py @@ -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(): diff --git a/backend/tests/sql/test_2464_land_reservation_dedup.py b/backend/tests/sql/test_2464_land_reservation_dedup.py index f9e923ee..067302af 100644 --- a/backend/tests/sql/test_2464_land_reservation_dedup.py +++ b/backend/tests/sql/test_2464_land_reservation_dedup.py @@ -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)" diff --git a/backend/tests/sql/test_2464_leads_stats_suffix_contract.py b/backend/tests/sql/test_2464_leads_stats_suffix_contract.py index fc13f048..f83a868f 100644 --- a/backend/tests/sql/test_2464_leads_stats_suffix_contract.py +++ b/backend/tests/sql/test_2464_leads_stats_suffix_contract.py @@ -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 diff --git a/backend/tests/sql/test_2956_freshness_ignores_failed_dumps.py b/backend/tests/sql/test_2956_freshness_ignores_failed_dumps.py index 7193158c..c30077ec 100644 --- a/backend/tests/sql/test_2956_freshness_ignores_failed_dumps.py +++ b/backend/tests/sql/test_2956_freshness_ignores_failed_dumps.py @@ -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"], ( - "последняя попытка должна быть новее последнего успеха — иначе провалы " "не видны вообще" + "последняя попытка должна быть новее последнего успеха — иначе провалы не видны вообще" ) diff --git a/backend/tests/sql/test_2986_permits_source_key.py b/backend/tests/sql/test_2986_permits_source_key.py index b4b2d675..1214708c 100644 --- a/backend/tests/sql/test_2986_permits_source_key.py +++ b/backend/tests/sql/test_2986_permits_source_key.py @@ -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() diff --git a/backend/tests/sql/test_2998_rosreestr_partition_horizon.py b/backend/tests/sql/test_2998_rosreestr_partition_horizon.py index 1275c420..1236bade 100644 --- a/backend/tests/sql/test_2998_rosreestr_partition_horizon.py +++ b/backend/tests/sql/test_2998_rosreestr_partition_horizon.py @@ -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" diff --git a/backend/tests/sql/test_auth_sql_migrations.py b/backend/tests/sql/test_auth_sql_migrations.py index 29a14105..cd446547 100644 --- a/backend/tests/sql/test_auth_sql_migrations.py +++ b/backend/tests/sql/test_auth_sql_migrations.py @@ -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." + ) diff --git a/backend/tests/sql/test_ddu_price_indicator.py b/backend/tests/sql/test_ddu_price_indicator.py index df9bd034..9033a404 100644 --- a/backend/tests/sql/test_ddu_price_indicator.py +++ b/backend/tests/sql/test_ddu_price_indicator.py @@ -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. diff --git a/backend/tests/sql/test_velocity_alerts.py b/backend/tests/sql/test_velocity_alerts.py index 44b34bfe..7baf7631 100644 --- a/backend/tests/sql/test_velocity_alerts.py +++ b/backend/tests/sql/test_velocity_alerts.py @@ -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] diff --git a/backend/tests/test_2464c_photos_session_release.py b/backend/tests/test_2464c_photos_session_release.py index 10b2a9db..dae3c691 100644 --- a/backend/tests/test_2464c_photos_session_release.py +++ b/backend/tests/test_2464c_photos_session_release.py @@ -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() действительно доехала до БД — читаем ОТДЕЛЬНЫМ diff --git a/backend/tests/test_2867_avg_area_nullable.py b/backend/tests/test_2867_avg_area_nullable.py index b9a96cfb..2f95a392 100644 --- a/backend/tests/test_2867_avg_area_nullable.py +++ b/backend/tests/test_2867_avg_area_nullable.py @@ -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: diff --git a/backend/tests/test_audit_middleware.py b/backend/tests/test_audit_middleware.py index c02e684f..e046d1a0 100644 --- a/backend/tests/test_audit_middleware.py +++ b/backend/tests/test_audit_middleware.py @@ -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", + ) # --------------------------------------------------------------------------- diff --git a/backend/tests/test_gas_grs_loader.py b/backend/tests/test_gas_grs_loader.py index 54687e72..4725dc3a 100644 --- a/backend/tests/test_gas_grs_loader.py +++ b/backend/tests/test_gas_grs_loader.py @@ -167,8 +167,7 @@ def test_parse_no_table_returns_empty() -> None: def test_parse_no_header_row_returns_empty() -> None: """Нет строки-заголовка (маркеры «наименование»+«проектн») → пусто (defensive).""" html = ( - "" - "
Колонка АКолонка Б
12
" + "
Колонка АКолонка Б
12
" ) assert gg.parse_grs_table(html) == [] diff --git a/backend/tests/test_gate_verdict.py b/backend/tests/test_gate_verdict.py index bc5bd67c..f0dfed29 100644 --- a/backend/tests/test_gate_verdict.py +++ b/backend/tests/test_gate_verdict.py @@ -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(): diff --git a/backend/tests/test_poi_score.py b/backend/tests/test_poi_score.py index ae5aeae3..97cfcffb 100644 --- a/backend/tests/test_poi_score.py +++ b/backend/tests/test_poi_score.py @@ -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(): diff --git a/backend/tests/test_quarter_dump_lookup.py b/backend/tests/test_quarter_dump_lookup.py index 15d1fdc1..bf58373b 100644 --- a/backend/tests/test_quarter_dump_lookup.py +++ b/backend/tests/test_quarter_dump_lookup.py @@ -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 diff --git a/backend/tests/test_sentry_init.py b/backend/tests/test_sentry_init.py index 19ff11d1..6abc2b9c 100644 --- a/backend/tests/test_sentry_init.py +++ b/backend/tests/test_sentry_init.py @@ -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 diff --git a/backend/tests/workers/tasks/test_gas_outlet_sync.py b/backend/tests/workers/tasks/test_gas_outlet_sync.py index 4cd5c056..2da47ad4 100644 --- a/backend/tests/workers/tasks/test_gas_outlet_sync.py +++ b/backend/tests/workers/tasks/test_gas_outlet_sync.py @@ -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). diff --git a/backend/tests/workers/tasks/test_okn_objects_sync.py b/backend/tests/workers/tasks/test_okn_objects_sync.py index 466ed6dd..5984ca3d 100644 --- a/backend/tests/workers/tasks/test_okn_objects_sync.py +++ b/backend/tests/workers/tasks/test_okn_objects_sync.py @@ -86,9 +86,9 @@ def test_extract_coords_inverted_would_fail_bbox() -> None: inverted_lon = feature["geometry"]["coordinates"][0] # 56.83 → не в bbox [58–63] inverted_lat = feature["geometry"]["coordinates"][1] # 60.61 → не в bbox [55–58.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: diff --git a/backend/tests/workers/test_2464_objective_zombie_sweep.py b/backend/tests/workers/test_2464_objective_zombie_sweep.py index 25ce10d3..1291ea35 100644 --- a/backend/tests/workers/test_2464_objective_zombie_sweep.py +++ b/backend/tests/workers/test_2464_objective_zombie_sweep.py @@ -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}" + ) diff --git a/backend/tests/workers/test_nspd_geo.py b/backend/tests/workers/test_nspd_geo.py index 8ee58bfb..77e3381a 100644 --- a/backend/tests/workers/test_nspd_geo.py +++ b/backend/tests/workers/test_nspd_geo.py @@ -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( diff --git a/backend/tests/workers/test_scrape_freshness_check.py b/backend/tests/workers/test_scrape_freshness_check.py index 4e05af2e..b014daf9 100644 --- a/backend/tests/workers/test_scrape_freshness_check.py +++ b/backend/tests/workers/test_scrape_freshness_check.py @@ -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]: diff --git a/tradein-mvp/backend/app/api/v1/search.py b/tradein-mvp/backend/app/api/v1/search.py index 9580c2af..26d34ba3 100644 --- a/tradein-mvp/backend/app/api/v1/search.py +++ b/tradein-mvp/backend/app/api/v1/search.py @@ -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 diff --git a/tradein-mvp/backend/app/services/cache.py b/tradein-mvp/backend/app/services/cache.py index 7de08ac0..80b2465e 100644 --- a/tradein-mvp/backend/app/services/cache.py +++ b/tradein-mvp/backend/app/services/cache.py @@ -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) diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index 3248e13a..73880ec1 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -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, diff --git a/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py b/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py index ef896560..ca2aff59 100644 --- a/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py +++ b/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py @@ -1665,9 +1665,12 @@ def _build_deals_page(estimate: AggregatedEstimate, input_snapshot: dict, brand) - {f''' + { + f'''''' - if deals_as_of else ""} + if deals_as_of + else "" + }
Количество сделок по аналогичном объектам {_mono(f"{n_deals} шт.")}
Сделки
Сделки {_mono(deals_as_of)}
Источники данных
diff --git a/tradein-mvp/backend/app/services/house_metadata.py b/tradein-mvp/backend/app/services/house_metadata.py index ff2d1f87..c69844c8 100644 --- a/tradein-mvp/backend/app/services/house_metadata.py +++ b/tradein-mvp/backend/app/services/house_metadata.py @@ -27,9 +27,9 @@ from app.core.config import settings logger = logging.getLogger(__name__) OVERPASS_URL = "https://overpass-api.de/api/interpreter" -_CACHE_RADIUS_M = 40 # дом в пределах 40 м от точки — считаем тем же -_OVERPASS_RADIUS_M = 25 # ищем здание в 25 м от геокодированной точки -_CAD_RADIUS_M = 60 # радиус поиска здания в cad_buildings (#393) +_CACHE_RADIUS_M = 40 # дом в пределах 40 м от точки — считаем тем же +_OVERPASS_RADIUS_M = 25 # ищем здание в 25 м от геокодированной точки +_CAD_RADIUS_M = 60 # радиус поиска здания в cad_buildings (#393) @dataclass(frozen=True, slots=True) @@ -38,15 +38,18 @@ class HouseMetadata: lon: float year_built: int | None total_floors: int | None - house_type: str | None # panel / brick / monolith / monolith_brick / other + house_type: str | None # panel / brick / monolith / monolith_brick / other total_units: int | None - source: str # 'osm' / 'cache' + source: str # 'osm' / 'cache' # ── Парсинг тегов OSM ──────────────────────────────────────────────────────── _YEAR_KEYS = ( - "start_date", "construction_date", "building:year", - "year_of_construction", "building:start_date", + "start_date", + "construction_date", + "building:year", + "year_of_construction", + "building:start_date", ) @@ -110,9 +113,12 @@ def _cache_get(db: Session, lat: float, lon: float) -> HouseMetadata | None: if row is None: return None return HouseMetadata( - lat=row.lat, lon=row.lon, - year_built=row.year_built, total_floors=row.total_floors, - house_type=row.house_type, total_units=row.total_units, + lat=row.lat, + lon=row.lon, + year_built=row.year_built, + total_floors=row.total_floors, + house_type=row.house_type, + total_units=row.total_units, source="cache", ) @@ -129,9 +135,12 @@ def _cache_put(db: Session, meta: HouseMetadata, raw: dict) -> None: """ ), { - "lat": meta.lat, "lon": meta.lon, - "year": meta.year_built, "floors": meta.total_floors, - "htype": meta.house_type, "units": meta.total_units, + "lat": meta.lat, + "lon": meta.lon, + "year": meta.year_built, + "floors": meta.total_floors, + "htype": meta.house_type, + "units": meta.total_units, "raw": json.dumps(raw, ensure_ascii=False), }, ) @@ -184,10 +193,11 @@ def _cad_buildings_get(db: Session, lat: float, lon: float) -> HouseMetadata | N if row is None: return None return HouseMetadata( - lat=lat, lon=lon, + lat=lat, + lon=lon, year_built=row.year_built, total_floors=row.floors, - house_type=None, # purpose не маппится в panel/brick + house_type=None, # purpose не маппится в panel/brick total_units=None, source="cadastre", ) @@ -219,7 +229,8 @@ async def _overpass_lookup(lat: float, lon: float) -> tuple[HouseMetadata, dict] best = min(elements, key=_dist2) tags = best.get("tags", {}) meta = HouseMetadata( - lat=lat, lon=lon, + lat=lat, + lon=lon, year_built=_parse_year(tags), total_floors=_parse_int(tags, "building:levels"), house_type=_parse_house_type(tags), @@ -242,7 +253,10 @@ async def get_house_metadata(lat: float, lon: float, db: Session) -> HouseMetada if cad is not None: logger.info( "house_metadata: cad_buildings (%.5f, %.5f) → year=%s floors=%s", - lat, lon, cad.year_built, cad.total_floors, + lat, + lon, + cad.year_built, + cad.total_floors, ) return cad @@ -264,13 +278,15 @@ async def get_house_metadata(lat: float, lon: float, db: Session) -> HouseMetada _cache_put(db, meta, raw) logger.info( "house_metadata OSM: (%.5f, %.5f) → year=%s floors=%s type=%s", - lat, lon, meta.year_built, meta.total_floors, meta.house_type, + lat, + lon, + meta.year_built, + meta.total_floors, + meta.house_type, ) return meta except Exception: - logger.warning( - "house_metadata enrichment failed at (%.5f, %.5f)", lat, lon, exc_info=True - ) + logger.warning("house_metadata enrichment failed at (%.5f, %.5f)", lat, lon, exc_info=True) try: db.rollback() except Exception: diff --git a/tradein-mvp/backend/app/services/matching/houses.py b/tradein-mvp/backend/app/services/matching/houses.py index 1effa723..531cb1fc 100644 --- a/tradein-mvp/backend/app/services/matching/houses.py +++ b/tradein-mvp/backend/app/services/matching/houses.py @@ -191,8 +191,7 @@ def match_or_create_house( row = ( db.execute( text( - "SELECT house_id FROM house_sources " - "WHERE ext_source = :s AND ext_id = :e LIMIT 1" + "SELECT house_id FROM house_sources WHERE ext_source = :s AND ext_id = :e LIMIT 1" ), {"s": ext_source, "e": str(ext_id)}, ) diff --git a/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py b/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py index 00d91cf9..affa088b 100644 --- a/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py @@ -125,8 +125,7 @@ def _alert_domclick_cookies(db: Session, run_id: int) -> None: detail = "кук DomClick нет в БД" elif expires_at <= now: detail = ( - f"куки DomClick протухли {expires_at:%Y-%m-%d} " - f"({(now - expires_at).days} дн. назад)" + f"куки DomClick протухли {expires_at:%Y-%m-%d} ({(now - expires_at).days} дн. назад)" ) else: detail = "куки DomClick помечены невалидными (last_invalid_at)" @@ -292,7 +291,7 @@ async def run_domclick_detail_backfill( # #1182 Phase 2: кооперативный SIGTERM-drain (деплой recreate scraper). if shutdown_requested(): logger.info( - "domclick_detail_backfill: run_id=%d SIGTERM-drain — stopping at " "#%d/%d", + "domclick_detail_backfill: run_id=%d SIGTERM-drain — stopping at #%d/%d", run_id, idx, len(snapshot), @@ -331,8 +330,7 @@ async def run_domclick_detail_backfill( consecutive_blocks += 1 counters.blocked += 1 logger.warning( - "domclick_detail_backfill: run_id=%d BLOCKED #%d/%d " - "(consecutive=%d): %s", + "domclick_detail_backfill: run_id=%d BLOCKED #%d/%d (consecutive=%d): %s", run_id, idx + 1, len(snapshot), diff --git a/tradein-mvp/backend/scripts/backfill_houses_dadata.py b/tradein-mvp/backend/scripts/backfill_houses_dadata.py index ec7dd2be..cabbd302 100644 --- a/tradein-mvp/backend/scripts/backfill_houses_dadata.py +++ b/tradein-mvp/backend/scripts/backfill_houses_dadata.py @@ -158,14 +158,11 @@ class Stats: # для `--priority listings`. Correlated EXISTS бьёт по partial-индексу # `listings_house_id_fk_idx (house_id_fk) WHERE house_id_fk IS NOT NULL`. _HAS_ACTIVE_LISTINGS_EXPR = ( - "EXISTS (SELECT 1 FROM listings l " - "WHERE l.house_id_fk = houses.id AND l.is_active)" + "EXISTS (SELECT 1 FROM listings l WHERE l.house_id_fk = houses.id AND l.is_active)" ) -def _select_candidates( - db: Session, *, priority: str, limit: int -) -> list[tuple[HouseRow, str]]: +def _select_candidates(db: Session, *, priority: str, limit: int) -> list[tuple[HouseRow, str]]: """Возвращает список (HouseRow, priority_bucket) для enrichment. priority: @@ -477,9 +474,7 @@ async def _run_backfill( db.rollback() stats.failed += 1 stats.bump(priority, "failed") - logger.warning( - "db_write failed for house_id=%s: %s", row.id, exc - ) + logger.warning("db_write failed for house_id=%s: %s", row.id, exc) if i % _LOG_EVERY == 0: logger.info( @@ -568,9 +563,7 @@ async def main(argv: list[str] | None = None) -> int: db = SessionLocal() try: candidates = _select_candidates(db, priority=args.priority, limit=args.limit) - logger.info( - "loaded candidates: %d (priority=%s)", len(candidates), args.priority - ) + logger.info("loaded candidates: %d (priority=%s)", len(candidates), args.priority) if not candidates: logger.info( "nothing to do — нет rows с dadata_enriched_at IS NULL для priority=%s", @@ -578,9 +571,7 @@ async def main(argv: list[str] | None = None) -> int: ) return 0 - stats = await _run_backfill( - db, candidates, batch=args.batch, dry_run=args.dry_run - ) + stats = await _run_backfill(db, candidates, batch=args.batch, dry_run=args.dry_run) logger.info( "done: batch=%s processed=%d enriched=%d no_change=%d " diff --git a/tradein-mvp/backend/scripts/geocode_deals_from_houses.py b/tradein-mvp/backend/scripts/geocode_deals_from_houses.py index fc1acc15..d924e67d 100644 --- a/tradein-mvp/backend/scripts/geocode_deals_from_houses.py +++ b/tradein-mvp/backend/scripts/geocode_deals_from_houses.py @@ -293,17 +293,21 @@ def _build_centroid_map(db: Session) -> dict[str, Centroid]: would require duplicating the regex logic in plpgsql and risk drift. 8,600 rows is trivial to hold in memory. """ - rows = db.execute( - text( - "SELECT address, lat, lon " - "FROM houses " - "WHERE geom IS NOT NULL " - " AND lat IS NOT NULL " - " AND lon IS NOT NULL " - " AND address IS NOT NULL " - " AND length(trim(address)) > 0" + rows = ( + db.execute( + text( + "SELECT address, lat, lon " + "FROM houses " + "WHERE geom IS NOT NULL " + " AND lat IS NOT NULL " + " AND lon IS NOT NULL " + " AND address IS NOT NULL " + " AND length(trim(address)) > 0" + ) ) - ).mappings().all() + .mappings() + .all() + ) # street_key → running [lat_sum, lon_sum, n] acc: dict[str, list[float]] = {} @@ -328,18 +332,22 @@ def _select_deals_without_coords(db: Session, limit: int) -> list[DealRow]: Matches `deals_geocode_pending_idx` (WHERE lat IS NULL). A successful UPDATE sets lat NOT NULL, dropping the row out on the next run. """ - rows = db.execute( - text( - "SELECT id, address " - "FROM deals " - "WHERE lat IS NULL " - " AND address IS NOT NULL " - " AND length(trim(address)) > 0 " - "ORDER BY id " - "LIMIT CAST(:lim AS int)" - ), - {"lim": limit}, - ).mappings().all() + rows = ( + db.execute( + text( + "SELECT id, address " + "FROM deals " + "WHERE lat IS NULL " + " AND address IS NOT NULL " + " AND length(trim(address)) > 0 " + "ORDER BY id " + "LIMIT CAST(:lim AS int)" + ), + {"lim": limit}, + ) + .mappings() + .all() + ) return [DealRow(id=r["id"], address=r["address"]) for r in rows] @@ -415,9 +423,7 @@ def _run_backfill( else: try: with db.begin_nested(): - _update_deal_coords( - db, deal_id=deal.id, lat=centroid.lat, lon=centroid.lon - ) + _update_deal_coords(db, deal_id=deal.id, lat=centroid.lat, lon=centroid.lon) # Per-row commit so resume picks up exactly where we crashed. db.commit() stats.geocoded += 1 @@ -470,9 +476,7 @@ def _report_dry_run( logger.info("─" * 60) logger.info("DRY-RUN SUMMARY (no DB writes)") logger.info("distinct streets with a house centroid: %d", distinct_streets) - logger.info( - "deals scanned this run (lat IS NULL, capped by --limit): %d", scanned - ) + logger.info("deals scanned this run (lat IS NULL, capped by --limit): %d", scanned) logger.info("deals matched to a centroid: %d", matched) logger.info("deals with no street match: %d", stats.no_street_match) logger.info("match rate on scanned slice: %.1f%%", match_rate * 100.0) @@ -550,9 +554,7 @@ def main(argv: list[str] | None = None) -> int: centroids = _build_centroid_map(db) logger.info("built centroid map: %d distinct streets", len(centroids)) if not centroids: - logger.warning( - "no house centroids — houses table has no geocoded rows; nothing to do" - ) + logger.warning("no house centroids — houses table has no geocoded rows; nothing to do") return 0 deals = _select_deals_without_coords(db, args.limit) @@ -561,9 +563,7 @@ def main(argv: list[str] | None = None) -> int: logger.info("nothing to do — no deals with lat IS NULL and an address") return 0 - stats = _run_backfill( - db, deals, centroids, batch=args.batch, dry_run=args.dry_run - ) + stats = _run_backfill(db, deals, centroids, batch=args.batch, dry_run=args.dry_run) if args.dry_run: total_null = _count_deals_null(db) diff --git a/tradein-mvp/backend/scripts/geocode_deals_nominatim.py b/tradein-mvp/backend/scripts/geocode_deals_nominatim.py index c6876d83..8bdeda9a 100644 --- a/tradein-mvp/backend/scripts/geocode_deals_nominatim.py +++ b/tradein-mvp/backend/scripts/geocode_deals_nominatim.py @@ -461,8 +461,7 @@ def _maybe_log_progress(i: int, groups: list[AddressGroup], batch: str, stats: S """Emit a progress line every `_LOG_EVERY` distinct addresses.""" if i % _LOG_EVERY == 0: logger.info( - "batch=%s progress %d/%d geocoded=%d failed=%d deals_updated=%d " - "cache=(hit=%d miss=%d)", + "batch=%s progress %d/%d geocoded=%d failed=%d deals_updated=%d cache=(hit=%d miss=%d)", batch, i, len(groups), diff --git a/tradein-mvp/backend/tests/scripts/test_backfill_houses_dadata.py b/tradein-mvp/backend/tests/scripts/test_backfill_houses_dadata.py index ea4711bf..a1633511 100644 --- a/tradein-mvp/backend/tests/scripts/test_backfill_houses_dadata.py +++ b/tradein-mvp/backend/tests/scripts/test_backfill_houses_dadata.py @@ -251,8 +251,12 @@ def test_update_house_enriched_passes_full_payload(): """UPDATE houses содержит lat/lon/cadnum/fias/qc-codes и dadata_enriched_at = NOW().""" db = MagicMock() res = _make_dadata_result( - qc_geo=0, qc_house=2, lat=56.838, lon=60.586, - cadnum="66:41:0704045:350", fias="fias-uuid-here", + qc_geo=0, + qc_house=2, + lat=56.838, + lon=60.586, + cadnum="66:41:0704045:350", + fias="fias-uuid-here", ) _update_house_enriched(db, house_id=42, result=res) @@ -304,9 +308,7 @@ async def test_run_backfill_qc_geo_0_writes_enriched_update(): "scripts.backfill_houses_dadata.clean_address", new=AsyncMock(return_value=fake), ): - stats = await _run_backfill( - db, [(row, "coords")], batch="b1", dry_run=False - ) + stats = await _run_backfill(db, [(row, "coords")], batch="b1", dry_run=False) assert stats.enriched == 1 assert stats.no_change == 0 @@ -338,9 +340,7 @@ async def test_run_backfill_qc_geo_3_records_attempt_only(): "scripts.backfill_houses_dadata.clean_address", new=AsyncMock(return_value=fake), ): - stats = await _run_backfill( - db, [(row, "coords")], batch="b2", dry_run=False - ) + stats = await _run_backfill(db, [(row, "coords")], batch="b2", dry_run=False) assert stats.enriched == 0 assert stats.no_change == 1 @@ -366,9 +366,7 @@ async def test_run_backfill_dadata_returns_none_records_attempt(): "scripts.backfill_houses_dadata.clean_address", new=AsyncMock(return_value=None), ): - stats = await _run_backfill( - db, [(row, "coords")], batch="b3", dry_run=False - ) + stats = await _run_backfill(db, [(row, "coords")], batch="b3", dry_run=False) assert stats.no_change == 1 assert stats.enriched == 0 @@ -425,9 +423,7 @@ async def test_run_backfill_dry_run_skips_db_writes(): "scripts.backfill_houses_dadata.clean_address", new=AsyncMock(return_value=fake), ): - stats = await _run_backfill( - db, [(row, "coords")], batch="dry", dry_run=True - ) + stats = await _run_backfill(db, [(row, "coords")], batch="dry", dry_run=True) assert stats.processed == 1 assert stats.enriched == 1 diff --git a/tradein-mvp/backend/tests/scripts/test_geocode_deals_from_houses.py b/tradein-mvp/backend/tests/scripts/test_geocode_deals_from_houses.py index b94e1202..a7c05e8e 100644 --- a/tradein-mvp/backend/tests/scripts/test_geocode_deals_from_houses.py +++ b/tradein-mvp/backend/tests/scripts/test_geocode_deals_from_houses.py @@ -105,10 +105,7 @@ def test_street_key_three_spec_variants_collapse_to_same_key(): def test_street_key_strips_region_and_house_number(): - assert ( - _street_key("Свердловская обл., Екатеринбург, ул. Большакова, 17") - == "большакова" - ) + assert _street_key("Свердловская обл., Екатеринбург, ул. Большакова, 17") == "большакова" def test_street_key_strips_district_marker(): @@ -139,10 +136,7 @@ def test_street_key_does_not_eat_name_starting_like_type_token(): def test_street_key_strips_korpus_and_kv_suffix(): assert _street_key("Екатеринбург, ул. Крауля, 48, корп. 2") == "крауля" - assert ( - _street_key("РФ, Свердловская обл., Екатеринбург, ул. Ленина, 5, кв. 12") - == "ленина" - ) + assert _street_key("РФ, Свердловская обл., Екатеринбург, ул. Ленина, 5, кв. 12") == "ленина" def test_street_key_does_not_eat_names_starting_like_apt_suffix(): diff --git a/tradein-mvp/backend/tests/services/test_location_index.py b/tradein-mvp/backend/tests/services/test_location_index.py index cee70c8c..9fa62103 100644 --- a/tradein-mvp/backend/tests/services/test_location_index.py +++ b/tradein-mvp/backend/tests/services/test_location_index.py @@ -173,9 +173,9 @@ def test_freshness_window_is_the_estimator_constant_not_a_copy() -> None: from app.services import estimator - assert "LISTINGS_FRESH_DAYS =" not in inspect.getsource( - lc - ), "константа скопирована в location_index — она должна ИМПОРТИРОВАТЬСЯ из estimator" + assert "LISTINGS_FRESH_DAYS =" not in inspect.getsource(lc), ( + "константа скопирована в location_index — она должна ИМПОРТИРОВАТЬСЯ из estimator" + ) assert lc.LISTINGS_FRESH_DAYS == estimator.LISTINGS_FRESH_DAYS diff --git a/tradein-mvp/backend/tests/services/test_proxy_rotation.py b/tradein-mvp/backend/tests/services/test_proxy_rotation.py index a311b7c9..b4fb5b0b 100644 --- a/tradein-mvp/backend/tests/services/test_proxy_rotation.py +++ b/tradein-mvp/backend/tests/services/test_proxy_rotation.py @@ -538,9 +538,9 @@ async def test_token_never_appears_in_log_messages_or_sentry_text( await proxy_rotation.rotate_proxy(db, 1) # type: ignore[arg-type] for record in caplog.records: - assert ( - SECRET_TOKEN not in record.getMessage() - ), f"scenario={name}: token leaked into log message args" + assert SECRET_TOKEN not in record.getMessage(), ( + f"scenario={name}: token leaked into log message args" + ) assert sentry_texts, "expected at least one Sentry capture (401 scenario)" assert all(SECRET_TOKEN not in text for text in sentry_texts) diff --git a/tradein-mvp/backend/tests/support/identity_modes.py b/tradein-mvp/backend/tests/support/identity_modes.py index 73c5672a..66d885ec 100644 --- a/tradein-mvp/backend/tests/support/identity_modes.py +++ b/tradein-mvp/backend/tests/support/identity_modes.py @@ -111,8 +111,7 @@ def assert_reads_access_state(sql: str, names: SqlNames) -> None: expected = f"{names.access_state_column} AS access_state" if expected not in sql: raise AssertionError( - f"запрос к реестру не читает колонку состояния текущего режима " - f"({expected!r}): {sql!r}" + f"запрос к реестру не читает колонку состояния текущего режима ({expected!r}): {sql!r}" ) diff --git a/tradein-mvp/backend/tests/tasks/test_geocode_missing.py b/tradein-mvp/backend/tests/tasks/test_geocode_missing.py index 0e786ffb..fe362774 100644 --- a/tradein-mvp/backend/tests/tasks/test_geocode_missing.py +++ b/tradein-mvp/backend/tests/tasks/test_geocode_missing.py @@ -680,9 +680,9 @@ def test_estimator_fetch_analogs_includes_avito() -> None: from app.services import estimator source = inspect.getsource(estimator._fetch_analogs) - assert ( - "source <> 'avito'" not in source - ), "Avito exclusion должен быть удалён из estimator._fetch_analogs" + assert "source <> 'avito'" not in source, ( + "Avito exclusion должен быть удалён из estimator._fetch_analogs" + ) # ── Admin endpoint smoke (schema-only, no real DB) ──────────────────────────── diff --git a/tradein-mvp/backend/tests/tasks/test_yandex_address_backfill.py b/tradein-mvp/backend/tests/tasks/test_yandex_address_backfill.py index 2e59eb54..ff51444b 100644 --- a/tradein-mvp/backend/tests/tasks/test_yandex_address_backfill.py +++ b/tradein-mvp/backend/tests/tasks/test_yandex_address_backfill.py @@ -38,9 +38,9 @@ def test_extract_address_standard_title() -> None: ) addr = _extract_address_from_title(html) assert addr is not None, "Should extract address from standard title" - assert _RE_HAS_HOUSE_NUMBER.search( - addr - ), f"Extracted address should contain house number: {addr!r}" + assert _RE_HAS_HOUSE_NUMBER.search(addr), ( + f"Extracted address should contain house number: {addr!r}" + ) assert "Екатеринбург" in addr assert "Горького" in addr assert "36" in addr @@ -54,9 +54,9 @@ def test_extract_address_with_zhk_prefix() -> None: ) addr = _extract_address_from_title(html) assert addr is not None, "Should extract address when ЖК prefix is present" - assert _RE_HAS_HOUSE_NUMBER.search( - addr - ), f"Extracted address should contain house number: {addr!r}" + assert _RE_HAS_HOUSE_NUMBER.search(addr), ( + f"Extracted address should contain house number: {addr!r}" + ) assert "Екатеринбург" in addr @@ -97,7 +97,7 @@ def test_extract_address_beryozovsky_with_zhk() -> None: def test_extract_address_ekb_no_zhk_still_matches() -> None: """EKB variant from docstring (no ЖК prefix) still matches after city-agnostic change.""" - html = "Продажа квартиры — Екатеринбург, улица Горького, 36" " — id 7654321" + html = "Продажа квартиры — Екатеринбург, улица Горького, 36 — id 7654321" addr = _extract_address_from_title(html) assert addr is not None, "EKB no-ЖК title should still match" assert _RE_HAS_HOUSE_NUMBER.search(addr), f"Should contain house number: {addr!r}" @@ -116,9 +116,9 @@ def test_extract_address_no_house_number_guard() -> None: addr = _extract_address_from_title(html) # If the regex matches at all, the extracted value must not contain a house number. if addr is not None: - assert not _RE_HAS_HOUSE_NUMBER.search( - addr - ), f"Street-only addr must fail house-number guard: {addr!r}" + assert not _RE_HAS_HOUSE_NUMBER.search(addr), ( + f"Street-only addr must fail house-number guard: {addr!r}" + ) def test_extract_address_nbsp_replaced() -> None: @@ -129,9 +129,9 @@ def test_extract_address_nbsp_replaced() -> None: ) addr = _extract_address_from_title(html) assert addr is not None, "\\xa0 should be replaced before regex" - assert _RE_HAS_HOUSE_NUMBER.search( - addr - ), f"Extracted address should contain house number: {addr!r}" + assert _RE_HAS_HOUSE_NUMBER.search(addr), ( + f"Extracted address should contain house number: {addr!r}" + ) def test_extract_address_no_title_returns_none() -> None: diff --git a/tradein-mvp/backend/tests/test_1781_secondary_only_param.py b/tradein-mvp/backend/tests/test_1781_secondary_only_param.py index 9ca89111..c4d474ab 100644 --- a/tradein-mvp/backend/tests/test_1781_secondary_only_param.py +++ b/tradein-mvp/backend/tests/test_1781_secondary_only_param.py @@ -38,9 +38,9 @@ def test_full_load_no_longer_hardcodes_secondary_only() -> None: src = inspect.getsource(pipeline.run_cian_full_load) assert "secondary_only=secondary_only" in src, "значение не пробрасывается из параметра" - assert ( - "secondary_only=True," not in src - ), "в теле остался хардкод — расписание на него повлиять не сможет" + assert "secondary_only=True," not in src, ( + "в теле остался хардкод — расписание на него повлиять не сможет" + ) def test_default_is_unchanged() -> None: @@ -101,12 +101,12 @@ def test_scraper_resets_the_counter_per_run() -> None: src = inspect.getsource(CianScraper.fetch_all_secondary) assert "self.last_dropped_nb = 0" in src, "нет сброса в начале прогона" - assert "self.last_dropped_nb += " in inspect.getsource( - CianScraper._paginate_leaf_bucket - ), "накопление не там, где считается dropped_nb" - assert ( - getattr(CianScraper, "last_dropped_nb", None) == 0 - ), "нет класс-дефолта: атрибут не прочитается, если прогон упал до первого бакета" + assert "self.last_dropped_nb += " in inspect.getsource(CianScraper._paginate_leaf_bucket), ( + "накопление не там, где считается dropped_nb" + ) + assert getattr(CianScraper, "last_dropped_nb", None) == 0, ( + "нет класс-дефолта: атрибут не прочитается, если прогон упал до первого бакета" + ) def test_filter_still_drops_when_flag_is_on() -> None: diff --git a/tradein-mvp/backend/tests/test_2674_writers_honor_schema.py b/tradein-mvp/backend/tests/test_2674_writers_honor_schema.py index 188b185d..0a9c69aa 100644 --- a/tradein-mvp/backend/tests/test_2674_writers_honor_schema.py +++ b/tradein-mvp/backend/tests/test_2674_writers_honor_schema.py @@ -142,9 +142,9 @@ def test_house_suggestions_insert_covers_every_declared_column() -> None: (_SQL_DIR / "064_house_imv_phase_c.sql").read_text("utf-8"), "house_suggestions" ) written = _insert_columns(inspect.getsource(hib.save_imv_result), "house_suggestions") - assert ( - declared - {"id"} <= written - ), f"колонки без писателя: {sorted(declared - {'id'} - written)}" + assert declared - {"id"} <= written, ( + f"колонки без писателя: {sorted(declared - {'id'} - written)}" + ) def test_save_imv_result_binds_image_link_and_metrics() -> None: diff --git a/tradein-mvp/backend/tests/test_2830_pool_bypass_tails.py b/tradein-mvp/backend/tests/test_2830_pool_bypass_tails.py index a6fb3820..8384a28a 100644 --- a/tradein-mvp/backend/tests/test_2830_pool_bypass_tails.py +++ b/tradein-mvp/backend/tests/test_2830_pool_bypass_tails.py @@ -147,9 +147,9 @@ async def test_price_history_takes_pool_node_despite_flag_off() -> None: """ from app.core.config import settings - assert ( - settings.use_proxy_pool_curl is False - ), "тест обязан идти тем же путём, что прод-контейнер backend: без USE_PROXY_POOL_CURL" + assert settings.use_proxy_pool_curl is False, ( + "тест обязан идти тем же путём, что прод-контейнер backend: без USE_PROXY_POOL_CURL" + ) pool = _SpyPool() await _run_price_history(pool, status_code=200) assert pool.acquire_calls == ["cian"] diff --git a/tradein-mvp/backend/tests/test_2924_yandex_resolve_tried_at.py b/tradein-mvp/backend/tests/test_2924_yandex_resolve_tried_at.py index b196303e..fbb26d18 100644 --- a/tradein-mvp/backend/tests/test_2924_yandex_resolve_tried_at.py +++ b/tradein-mvp/backend/tests/test_2924_yandex_resolve_tried_at.py @@ -139,9 +139,9 @@ async def test_failed_resolve_marks_the_house() -> None: db = await _run( [{"house_id": 6706, "yandex_jk_slug": None, "yandex_jk_id": None, "ext_id": "286394"}] ) - assert _tried_updates(db) == [ - 6706 - ], f"неудача резолва не помечена; выполненный SQL: {[s[:50] for s, _ in db.sql]}" + assert _tried_updates(db) == [6706], ( + f"неудача резолва не помечена; выполненный SQL: {[s[:50] for s, _ in db.sql]}" + ) @pytest.mark.asyncio @@ -174,7 +174,7 @@ async def test_force_bypasses_the_marker() -> None: db = await _run([], force=True) sql = _select_sql(db) # Тот же OR-блок, что и у гейта «уже обогащён»: CAST(:force AS boolean) = TRUE OR … - assert ( - sql.count("CAST(:force AS boolean) = TRUE") >= 2 - ), "маркер не обходится через force — второго OR-блока с :force нет" + assert sql.count("CAST(:force AS boolean) = TRUE") >= 2, ( + "маркер не обходится через force — второго OR-блока с :force нет" + ) assert _select_params(db).get("force") is True diff --git a/tradein-mvp/backend/tests/test_2936_unknown_attr_penalty.py b/tradein-mvp/backend/tests/test_2936_unknown_attr_penalty.py index 1dea4cab..db4cd3f4 100644 --- a/tradein-mvp/backend/tests/test_2936_unknown_attr_penalty.py +++ b/tradein-mvp/backend/tests/test_2936_unknown_attr_penalty.py @@ -48,12 +48,12 @@ def test_null_year_gets_pool_median_penalty() -> None: by = {c["id"]: c["relevance_score"] for c in pool} # Известных годов ШЕСТЬ (ид. 1,2,3,4,5 и 7): штрафы 0,1,2,3,4 и 0 → медиана 1.5. # (Первая редакция теста считала пятерых и ждала 2.0 — арифметика, не код.) - assert ( - by[6] == 0.5 + 1.5 - ), f"NULL-год должен получить медианный штраф 1.5, получил {by[6] - 0.5}" - assert ( - by[1] == 0.5 and by[3] == 0.5 - ), "известный год не должен трогаться — его уже оштрафовал SQL" + assert by[6] == 0.5 + 1.5, ( + f"NULL-год должен получить медианный штраф 1.5, получил {by[6] - 0.5}" + ) + assert by[1] == 0.5 and by[3] == 0.5, ( + "известный год не должен трогаться — его уже оштрафовал SQL" + ) def test_null_house_type_gets_pool_median_penalty() -> None: diff --git a/tradein-mvp/backend/tests/test_2953_nominatim_throttle.py b/tradein-mvp/backend/tests/test_2953_nominatim_throttle.py index cbe363a7..49c6bd60 100644 --- a/tradein-mvp/backend/tests/test_2953_nominatim_throttle.py +++ b/tradein-mvp/backend/tests/test_2953_nominatim_throttle.py @@ -84,8 +84,7 @@ async def test_consecutive_queries_are_spaced(throttle_reset: None) -> None: assert len(stamps) == 3, f"ожидали 3 запроса, ушло {len(stamps)}" gaps = _gaps(stamps) assert all(g >= _TEST_INTERVAL * 0.9 for g in gaps), ( - f"запросы идут вплотную: зазоры {[round(g, 4) for g in gaps]}, " - f"ожидалось ≥ {_TEST_INTERVAL}" + f"запросы идут вплотную: зазоры {[round(g, 4) for g in gaps]}, ожидалось ≥ {_TEST_INTERVAL}" ) @@ -108,8 +107,7 @@ async def test_failed_lookup_does_not_reset_the_pace(throttle_reset: None) -> No await geocoder._nominatim_lookup("ненаходимый адрес два") assert boundary_index > 0 and len(stamps) > boundary_index, ( - f"оба поиска должны были сходить в сеть: {len(stamps)} запросов, " - f"граница {boundary_index}" + f"оба поиска должны были сходить в сеть: {len(stamps)} запросов, граница {boundary_index}" ) boundary_gap = stamps[boundary_index] - stamps[boundary_index - 1] assert boundary_gap >= _TEST_INTERVAL * 0.9, ( diff --git a/tradein-mvp/backend/tests/test_2992_upsert_unchanged_gate.py b/tradein-mvp/backend/tests/test_2992_upsert_unchanged_gate.py index 0b3a0703..dbe3c0ca 100644 --- a/tradein-mvp/backend/tests/test_2992_upsert_unchanged_gate.py +++ b/tradein-mvp/backend/tests/test_2992_upsert_unchanged_gate.py @@ -117,9 +117,9 @@ def test_unchanged_rescrape_same_day_does_not_update_the_row() -> None: assert first is not None, "первая вставка не прошла" ins, upd = save_listings(db, [_lot(sid)], matcher=_matcher(), region_code=66) second = _row(db, sid) - assert ( - second.ctid == first.ctid - ), f"повторный скрейп неизменного лота переписал строку: ctid {first.ctid}→{second.ctid}" + assert second.ctid == first.ctid, ( + f"повторный скрейп неизменного лота переписал строку: ctid {first.ctid}→{second.ctid}" + ) assert (ins, upd) == (0, 0), f"счётчики: inserted={ins} updated={upd}, ждали 0/0" assert second.last_seen_at == first.last_seen_at, "last_seen_at сдвинулся без нужды" finally: @@ -204,13 +204,13 @@ def test_skipped_row_still_yields_listing_id_for_downstream() -> None: try: save_listings(db, [lot], matcher=m, region_code=66) calls_before = m.match_or_create_house.call_count - assert ( - calls_before == 1 - ), "контроль сконструирован неверно: матчинг не зовётся и в первый раз" + assert calls_before == 1, ( + "контроль сконструирован неверно: матчинг не зовётся и в первый раз" + ) save_listings(db, [lot], matcher=m, region_code=66) - assert ( - m.match_or_create_house.call_count == calls_before + 1 - ), "при пропущенном апдейте матчинг не вызван — listing_id потерян" + assert m.match_or_create_house.call_count == calls_before + 1, ( + "при пропущенном апдейте матчинг не вызван — listing_id потерян" + ) finally: _cleanup(db) @@ -245,14 +245,14 @@ def test_listing_sources_unchanged_rescrape_same_day_does_not_update() -> None: upsert_listing_source(db, listing_id=lid, price_rub=5_000_000, **kw) db.commit() second = ls_row() - assert ( - second.ctid == first.ctid - ), f"listing_sources переписана без изменений: ctid {first.ctid}→{second.ctid}" + assert second.ctid == first.ctid, ( + f"listing_sources переписана без изменений: ctid {first.ctid}→{second.ctid}" + ) upsert_listing_source(db, listing_id=lid, price_rub=5_100_000, **kw) db.commit() third = ls_row() - assert ( - third.ctid != second.ctid and third.price_rub == 5_100_000 - ), "изменение цены не записалось" + assert third.ctid != second.ctid and third.price_rub == 5_100_000, ( + "изменение цены не записалось" + ) finally: _cleanup(db) diff --git a/tradein-mvp/backend/tests/test_2996_mislocated_houses_watchdog.py b/tradein-mvp/backend/tests/test_2996_mislocated_houses_watchdog.py index 94d57a46..a216f3c8 100644 --- a/tradein-mvp/backend/tests/test_2996_mislocated_houses_watchdog.py +++ b/tradein-mvp/backend/tests/test_2996_mislocated_houses_watchdog.py @@ -51,7 +51,7 @@ def test_threshold_sits_in_the_measured_gap() -> None: порог = getattr(admin, "_MISLOCATED_KM", None) assert порог is not None, ( - "порога нет вовсе — сторож не задан, дома в Варшаве и Таллине никем " "не считаются" + "порога нет вовсе — сторож не задан, дома в Варшаве и Таллине никем не считаются" ) assert 46.7 < порог < 119.2, ( f"порог {порог} вне измеренного промежутка (46.7, 119.2): ниже — ловит " @@ -91,9 +91,9 @@ def test_query_compares_house_to_its_own_listings() -> None: src = str(запрос) assert "l.house_id_fk = h.id" in src, "дом не сравнивается со СВОИМИ объявлениями" assert "percentile_disc(0.5)" in src, "берётся не медиана координат объявлений" - assert not re.search( - r"ST_Y\(.*BETWEEN", src - ), "появилась географическая рамка — она не переживёт расширение региона" + assert not re.search(r"ST_Y\(.*BETWEEN", src), ( + "появилась географическая рамка — она не переживёт расширение региона" + ) def test_check_stays_out_of_the_polled_endpoint() -> None: @@ -106,7 +106,7 @@ def test_check_stays_out_of_the_polled_endpoint() -> None: from app.api.v1 import admin src = inspect.getsource(admin.get_data_quality) - assert ( - "percentile_disc" not in src - ), "тяжёлый агрегат вернулся в опрашиваемую каждые 2 минуты ручку" + assert "percentile_disc" not in src, ( + "тяжёлый агрегат вернулся в опрашиваемую каждые 2 минуты ручку" + ) assert "mislocated" not in src.lower(), "проверка снова подмешана в data-quality" diff --git a/tradein-mvp/backend/tests/test_781_quality_gate.py b/tradein-mvp/backend/tests/test_781_quality_gate.py index ac689067..26452d64 100644 --- a/tradein-mvp/backend/tests/test_781_quality_gate.py +++ b/tradein-mvp/backend/tests/test_781_quality_gate.py @@ -166,9 +166,9 @@ def test_755_anchor_n2_does_not_fire_headline_stays_radius() -> None: "anchor with n=2 comps must NOT fire (min_comps=4 post-#755)" ) # Confidence from 5 radius analogs must not be "high" (unique_addr < 7 threshold). - assert ( - est.confidence != "high" - ), f"Confidence should not be 'high' with 5 radius analogs, got {est.confidence!r}" + assert est.confidence != "high", ( + f"Confidence should not be 'high' with 5 radius analogs, got {est.confidence!r}" + ) def test_755_anchor_n2_pure_unit_confidence_never_high() -> None: @@ -250,9 +250,9 @@ def test_753_dedup_hash_stable_across_reprice_with_source_id() -> None: lot_original = _make_scraped_lot(source_url=url, source_id=sid, price_rub=5_000_000) lot_repriced = _make_scraped_lot(source_url=url, source_id=sid, price_rub=4_900_000) - assert ( - lot_original.compute_dedup_hash() == lot_repriced.compute_dedup_hash() - ), "dedup_hash must be stable across reprice when source_id is present (#753)" + assert lot_original.compute_dedup_hash() == lot_repriced.compute_dedup_hash(), ( + "dedup_hash must be stable across reprice when source_id is present (#753)" + ) def test_753_dedup_hash_stable_across_context_query_no_source_id() -> None: @@ -275,9 +275,9 @@ def test_753_dedup_hash_stable_across_context_query_no_source_id() -> None: source_id=None, price_rub=5_000_000, ) - assert ( - lot_v1.compute_dedup_hash() == lot_v2.compute_dedup_hash() - ), "dedup_hash must strip ?context= query and match for same listing URL (#753)" + assert lot_v1.compute_dedup_hash() == lot_v2.compute_dedup_hash(), ( + "dedup_hash must strip ?context= query and match for same listing URL (#753)" + ) def test_753_dedup_hash_differs_for_distinct_listings() -> None: @@ -312,9 +312,9 @@ def test_753_dedup_hash_source_id_takes_priority_over_url() -> None: source_url="https://www.avito.ru/ru/offer/9999?some=other", source_id="9999", ) - assert ( - lot_canonical.compute_dedup_hash() == lot_redirect.compute_dedup_hash() - ), "source_id takes priority: same source_id -> same hash regardless of URL (#753)" + assert lot_canonical.compute_dedup_hash() == lot_redirect.compute_dedup_hash(), ( + "source_id takes priority: same source_id -> same hash regardless of URL (#753)" + ) # --------------------------------------------------------------------------- @@ -353,12 +353,12 @@ def test_773_expected_sold_positive_on_anchor_only_path( # Anchor gave a valid headline. assert est.median_price_rub > 0, "anchor must produce non-zero headline" # expected_sold must also be computed (not skipped by empty listings_clean). - assert ( - est.expected_sold_price_rub is not None - ), "expected_sold_price_rub must not be None on anchor-only path when ratio present (#773)" - assert ( - est.expected_sold_price_rub > 0 - ), f"expected_sold_price_rub must be > 0, got {est.expected_sold_price_rub} (#773)" + assert est.expected_sold_price_rub is not None, ( + "expected_sold_price_rub must not be None on anchor-only path when ratio present (#773)" + ) + assert est.expected_sold_price_rub > 0, ( + f"expected_sold_price_rub must be > 0, got {est.expected_sold_price_rub} (#773)" + ) # Consistency: sold = asking * ratio. assert est.expected_sold_price_rub == round(est.median_price_rub * 0.92) @@ -376,9 +376,9 @@ def test_773_expected_sold_null_when_no_ratio_anchor_only() -> None: ratio_tuple=(None, None), ) assert est.median_price_rub > 0 # anchor headline is valid - assert ( - est.expected_sold_price_rub is None - ), "expected_sold must be None when ratio is None, even on anchor-only path (#773)" + assert est.expected_sold_price_rub is None, ( + "expected_sold must be None when ratio is None, even on anchor-only path (#773)" + ) # --------------------------------------------------------------------------- @@ -437,9 +437,9 @@ async def test_754_block_page_http200_page_gt1_graceful_return() -> None: result = await scraper.fetch_around(56.838, 60.605, radius_m=1000, pages=2) # The important invariant: no exception raised, result is a list. - assert isinstance( - result, list - ), "fetch_around must return a list, not raise, when page>1 returns 0 cards (#754)" + assert isinstance(result, list), ( + "fetch_around must return a list, not raise, when page>1 returns 0 cards (#754)" + ) # --------------------------------------------------------------------------- @@ -476,9 +476,9 @@ def test_740_median_zero_gives_insufficient_data_true() -> None: actual_deals=[], expires_at=datetime(2026, 6, 1, tzinfo=UTC), ) - assert ( - est.insufficient_data is True - ), "insufficient_data must be True when median_price_rub=0 (#740)" + assert est.insufficient_data is True, ( + "insufficient_data must be True when median_price_rub=0 (#740)" + ) assert est.median_price_rub == 0 @@ -505,9 +505,9 @@ def test_740_positive_median_gives_insufficient_data_false() -> None: actual_deals=[], expires_at=datetime(2026, 6, 1, tzinfo=UTC), ) - assert ( - est.insufficient_data is False - ), "insufficient_data must be False when median_price_rub>0 (#740)" + assert est.insufficient_data is False, ( + "insufficient_data must be False when median_price_rub>0 (#740)" + ) def test_740_insufficient_data_serialized_in_model_dump() -> None: diff --git a/tradein-mvp/backend/tests/test_alerts_become_events.py b/tradein-mvp/backend/tests/test_alerts_become_events.py index 22324746..82b0b0f7 100644 --- a/tradein-mvp/backend/tests/test_alerts_become_events.py +++ b/tradein-mvp/backend/tests/test_alerts_become_events.py @@ -155,9 +155,9 @@ def test_sber_pull_stall_becomes_event(monkeypatch: pytest.MonkeyPatch) -> None: now=datetime(2026, 8, 6, tzinfo=UTC), ) assert out["alert"] == 1 - assert any( - "sber freshness" in t for t in event_texts(events) - ), "отставание загрузки не стало событием — WARNING до GlitchTip не долетает" + assert any("sber freshness" in t for t in event_texts(events)), ( + "отставание загрузки не стало событием — WARNING до GlitchTip не долетает" + ) def test_sber_old_period_with_healthy_pull_stays_silent(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tradein-mvp/backend/tests/test_asking_to_sold_ratio.py b/tradein-mvp/backend/tests/test_asking_to_sold_ratio.py index 671d8f58..b12d02a5 100644 --- a/tradein-mvp/backend/tests/test_asking_to_sold_ratio.py +++ b/tradein-mvp/backend/tests/test_asking_to_sold_ratio.py @@ -494,13 +494,13 @@ def test_migration_098_band_matches_settings_default() -> None: from app.tasks.asking_to_sold_ratio import _PPM2_MIN # Lower bound — matches the 30000 literal in migration 098. - assert ( - _PPM2_MIN == 30_000 - ), f"_PPM2_MIN changed ({_PPM2_MIN}); update migration 098 seed literals to match" + assert _PPM2_MIN == 30_000, ( + f"_PPM2_MIN changed ({_PPM2_MIN}); update migration 098 seed literals to match" + ) # Upper bound — matches the 1200000 literal in migration 098. - assert ( - Settings().asking_ratio_ppm2_max == 1_200_000 - ), "asking_ratio_ppm2_max default changed; update migration 098 seed literals to match" + assert Settings().asking_ratio_ppm2_max == 1_200_000, ( + "asking_ratio_ppm2_max default changed; update migration 098 seed literals to match" + ) # ── area_bucket() Python twin matches _AREA_ROOMS_BUCKET_SQL (#2620) ───────── diff --git a/tradein-mvp/backend/tests/test_auth_api.py b/tradein-mvp/backend/tests/test_auth_api.py index 9cd7b5cd..9436d1f7 100644 --- a/tradein-mvp/backend/tests/test_auth_api.py +++ b/tradein-mvp/backend/tests/test_auth_api.py @@ -826,18 +826,18 @@ async def test_login_flood_capped_by_rate_while_api_stays_responsive( # этого вердикта, а не вместо него. assert probe_latencies, "проба не сделала ни одного запроса" probe_latencies.sort() - assert ( - probe_latencies[-1] < 0.5 - ), f"худший сторонний запрос {probe_latencies[-1] * 1000:.0f}мс — API встаёт под флудом входа" + assert probe_latencies[-1] < 0.5, ( + f"худший сторонний запрос {probe_latencies[-1] * 1000:.0f}мс — API встаёт под флудом входа" + ) median_probe = probe_latencies[len(probe_latencies) // 2] assert median_probe < verify_s, ( f"медиана стороннего запроса {median_probe * 1000:.0f}мс ≥ времени одной " f"сверки — цикл занят проверкой пароля, API стоит" ) # Мало проб за секунду — тоже занятый цикл: проба просыпается раз в 10мс. - assert ( - len(probe_latencies) >= 10 - ), f"проба успела всего {len(probe_latencies)} раз за {elapsed:.2f}с — цикл был занят" + assert len(probe_latencies) >= 10, ( + f"проба успела всего {len(probe_latencies)} раз за {elapsed:.2f}с — цикл был занят" + ) # 2. ТЕМП ограничен. Флуд предлагал больше попыток в секунду, чем разрешает # потолок — до bcrypt их доехало не больше него (запас ×1.5 на планировщик). diff --git a/tradein-mvp/backend/tests/test_backfill_wave2.py b/tradein-mvp/backend/tests/test_backfill_wave2.py index 8a386aa4..178986ab 100644 --- a/tradein-mvp/backend/tests/test_backfill_wave2.py +++ b/tradein-mvp/backend/tests/test_backfill_wave2.py @@ -59,17 +59,13 @@ class TestYandexTitleExtract: assert not result.endswith(",") def test_nbsp_replaced(self): - html = ( - "— Екатеринбург,\xa0улица Горького, 36 — id 12 " "на Яндекс.Недвижимости" - ) + html = "— Екатеринбург,\xa0улица Горького, 36 — id 12 на Яндекс.Недвижимости" result = self._extract(html) assert result is not None assert "\xa0" not in result def test_case_insensitive_city(self): - html = ( - "— ЕКАТЕРИНБУРГ, улица Горького, 36 — id 777 " "на Яндекс.Недвижимости" - ) + html = "— ЕКАТЕРИНБУРГ, улица Горького, 36 — id 777 на Яндекс.Недвижимости" # regex is case-insensitive; city name retains original case result = self._extract(html) assert result is not None @@ -101,8 +97,7 @@ async def test_backfill_yandex_addresses_saves_enriched(): from app.services.yandex_address_backfill import backfill_yandex_addresses html_with_addr = ( - "Продажа — Екатеринбург, улица Горького, 36 — id 11 " - "на Яндекс.Недвижимости" + "Продажа — Екатеринбург, улица Горького, 36 — id 11 на Яндекс.Недвижимости" ) mock_resp = MagicMock() diff --git a/tradein-mvp/backend/tests/test_backtest_regression_gate.py b/tradein-mvp/backend/tests/test_backtest_regression_gate.py index 51a26de8..0275db49 100644 --- a/tradein-mvp/backend/tests/test_backtest_regression_gate.py +++ b/tradein-mvp/backend/tests/test_backtest_regression_gate.py @@ -46,9 +46,9 @@ _ABS_TOL = 1e-6 def _assert_match(path: str, expected: object, actual: object) -> None: if isinstance(expected, dict): assert isinstance(actual, dict), f"{path}: expected dict, got {type(actual).__name__}" - assert ( - expected.keys() == actual.keys() - ), f"{path}: key set differs\n expected={sorted(expected)}\n actual= {sorted(actual)}" + assert expected.keys() == actual.keys(), ( + f"{path}: key set differs\n expected={sorted(expected)}\n actual= {sorted(actual)}" + ) for k in expected: _assert_match(f"{path}.{k}", expected[k], actual[k]) elif isinstance(expected, list): diff --git a/tradein-mvp/backend/tests/test_cian_state_parser.py b/tradein-mvp/backend/tests/test_cian_state_parser.py index e8d7e4c4..a8665672 100644 --- a/tradein-mvp/backend/tests/test_cian_state_parser.py +++ b/tradein-mvp/backend/tests/test_cian_state_parser.py @@ -28,9 +28,7 @@ def _concat_push(mfe: str, entries: list[tuple[str, str]]) -> str: (не эскейпленных дважды — `.concat([...])` несёт валидный JSON-массив как есть). """ items = ",".join(f'{{"key":"{k}","value":{v},"priority":0}}' for k, v in entries) - return ( - f"window._cianConfig['{mfe}'] = (window._cianConfig['{mfe}'] || [])" f".concat([{items}]);" - ) + return f"window._cianConfig['{mfe}'] = (window._cianConfig['{mfe}'] || []).concat([{items}]);" def _push(mfe: str, key: str, value: str) -> str: diff --git a/tradein-mvp/backend/tests/test_deactivate_stale_avito.py b/tradein-mvp/backend/tests/test_deactivate_stale_avito.py index 9dffac4c..fcac27cf 100644 --- a/tradein-mvp/backend/tests/test_deactivate_stale_avito.py +++ b/tradein-mvp/backend/tests/test_deactivate_stale_avito.py @@ -253,17 +253,17 @@ def test_migration_100_updates_correct_source() -> None: def test_migration_100_sets_enabled_true() -> None: sql = _MIGRATION_100.read_text("utf-8") # UPDATE … SET enabled = true - assert re.search( - r"enabled\s*=\s*true", sql, re.IGNORECASE - ), "migration 100 must SET enabled = true" + assert re.search(r"enabled\s*=\s*true", sql, re.IGNORECASE), ( + "migration 100 must SET enabled = true" + ) def test_migration_100_is_idempotent() -> None: """WHERE enabled = false ensures re-running after fix → 0 rows matched.""" sql = _MIGRATION_100.read_text("utf-8") - assert re.search( - r"enabled\s*=\s*false", sql, re.IGNORECASE - ), "migration 100 must guard with AND enabled = false for idempotency" + assert re.search(r"enabled\s*=\s*false", sql, re.IGNORECASE), ( + "migration 100 must guard with AND enabled = false for idempotency" + ) def test_migration_100_is_transactional() -> None: diff --git a/tradein-mvp/backend/tests/test_deactivate_stale_revisit_floor.py b/tradein-mvp/backend/tests/test_deactivate_stale_revisit_floor.py index fbfc49f5..5ea5fce5 100644 --- a/tradein-mvp/backend/tests/test_deactivate_stale_revisit_floor.py +++ b/tradein-mvp/backend/tests/test_deactivate_stale_revisit_floor.py @@ -157,9 +157,9 @@ def test_effective_ttl_covers_every_proven_false_kill(monkeypatch: pytest.Monkey ) _, update_params = db.update_query assert update_params is not None - assert ( - update_params["ttl_days"] == effective - ), f"{slice_name}: UPDATE получил не поднятый TTL — пол посчитан и выброшен" + assert update_params["ttl_days"] == effective, ( + f"{slice_name}: UPDATE получил не поднятый TTL — пол посчитан и выброшен" + ) def _read_cap_mult_from_migration(filename: str, *, source: str) -> int: diff --git a/tradein-mvp/backend/tests/test_dead_code_sweep_2674.py b/tradein-mvp/backend/tests/test_dead_code_sweep_2674.py index 5e6e0129..9e437ef9 100644 --- a/tradein-mvp/backend/tests/test_dead_code_sweep_2674.py +++ b/tradein-mvp/backend/tests/test_dead_code_sweep_2674.py @@ -82,8 +82,7 @@ def test_domrf_window_does_not_collide_with_matview_refresh() -> None: start, end = hours[0], hours[1] matview_start, matview_end = 3, 4 # прод-значение scrape_schedules на 2026-08-06 assert end <= matview_start or start >= matview_end, ( - f"окно {start}-{end} пересекается с refresh_search_matview " - f"{matview_start}-{matview_end}" + f"окно {start}-{end} пересекается с refresh_search_matview {matview_start}-{matview_end}" ) @@ -248,9 +247,9 @@ def test_price_divergence_is_documented_as_structurally_empty() -> None: """Оставленный задел обязан говорить, чем он НЕ является сегодня.""" sql = MIGRATION.read_text(encoding="utf-8") comment = sql.split("COMMENT ON VIEW v_price_divergence IS", 1)[1].split(";", 1)[0] - assert ( - "match_or_create_listing" in comment - ), "комментарий не называет причину пустоты — без неё это просто «пока пусто»" + assert "match_or_create_listing" in comment, ( + "комментарий не называет причину пустоты — без неё это просто «пока пусто»" + ) # ───────────────────────────────────────────────────────────────────────────── diff --git a/tradein-mvp/backend/tests/test_estimator_cohort.py b/tradein-mvp/backend/tests/test_estimator_cohort.py index b6c572fb..0a4682c0 100644 --- a/tradein-mvp/backend/tests/test_estimator_cohort.py +++ b/tradein-mvp/backend/tests/test_estimator_cohort.py @@ -3,6 +3,7 @@ PR 10 (2026-05-24) — covers _target_cohort_range edge cases: None, out-of-range, exact boundaries, first-match semantics. """ + import os # Settings requires DATABASE_URL at init time. Set dummy DSN before any app import. @@ -16,26 +17,26 @@ from app.services.estimator import _target_cohort_range @pytest.mark.parametrize( ("year", "expected"), [ - (None, None), # no year → no cohort - (1900, None), # out-of-range below → no cohort - (1954, None), # below khrushchev floor - (1955, (1955, 1969)), # khrushchev floor - (1960, (1955, 1969)), # khrushchev mid - (1969, (1955, 1969)), # khrushchev ceiling - (1970, (1970, 1989)), # brezhnev floor - (1978, (1970, 1989)), # brezhnev (target audit case) - (1988, (1970, 1989)), # brezhnev pre-overlap fix held - (1989, (1970, 1989)), # brezhnev ceiling - (1990, (1990, 1999)), # late_soviet floor (post-PR10 fix) - (1995, (1990, 1999)), # late_soviet mid - (1999, (1990, 1999)), # late_soviet ceiling - (2000, (2000, 2010)), # 2000s floor - (2010, (2000, 2010)), # 2000s ceiling - (2011, (2011, 2100)), # modern floor - (2024, (2011, 2100)), # modern current - (2100, (2011, 2100)), # modern ceiling - (2101, None), # above modern ceiling → no cohort - (2200, None), # far future → no cohort + (None, None), # no year → no cohort + (1900, None), # out-of-range below → no cohort + (1954, None), # below khrushchev floor + (1955, (1955, 1969)), # khrushchev floor + (1960, (1955, 1969)), # khrushchev mid + (1969, (1955, 1969)), # khrushchev ceiling + (1970, (1970, 1989)), # brezhnev floor + (1978, (1970, 1989)), # brezhnev (target audit case) + (1988, (1970, 1989)), # brezhnev pre-overlap fix held + (1989, (1970, 1989)), # brezhnev ceiling + (1990, (1990, 1999)), # late_soviet floor (post-PR10 fix) + (1995, (1990, 1999)), # late_soviet mid + (1999, (1990, 1999)), # late_soviet ceiling + (2000, (2000, 2010)), # 2000s floor + (2010, (2000, 2010)), # 2000s ceiling + (2011, (2011, 2100)), # modern floor + (2024, (2011, 2100)), # modern current + (2100, (2011, 2100)), # modern ceiling + (2101, None), # above modern ceiling → no cohort + (2200, None), # far future → no cohort ], ) def test_target_cohort_range(year, expected): diff --git a/tradein-mvp/backend/tests/test_estimator_confidence_reliability_consistency.py b/tradein-mvp/backend/tests/test_estimator_confidence_reliability_consistency.py index 16815488..ccaec2ae 100644 --- a/tradein-mvp/backend/tests/test_estimator_confidence_reliability_consistency.py +++ b/tradein-mvp/backend/tests/test_estimator_confidence_reliability_consistency.py @@ -57,9 +57,9 @@ def test_very_low_reliability_forces_confidence_low() -> None: """reliability == 'very_low' → confidence forced to 'low', regardless of what _compute_confidence originally scored.""" for original in ("high", "medium", "low"): - assert ( - _cap_confidence_by_reliability(original, "very_low") == "low" - ), f"original={original!r} must be forced to 'low' under very_low reliability" + assert _cap_confidence_by_reliability(original, "very_low") == "low", ( + f"original={original!r} must be forced to 'low' under very_low reliability" + ) def test_low_reliability_caps_confidence_at_medium() -> None: diff --git a/tradein-mvp/backend/tests/test_estimator_expected_sold.py b/tradein-mvp/backend/tests/test_estimator_expected_sold.py index 51e8a675..c0dece68 100644 --- a/tradein-mvp/backend/tests/test_estimator_expected_sold.py +++ b/tradein-mvp/backend/tests/test_estimator_expected_sold.py @@ -68,9 +68,9 @@ def test_bucket_clamping() -> None: _get_asking_sold_ratio(db, rooms) # First positional bind param is {"b": bucket}. bind = db.execute.call_args_list[0].args[1] - assert ( - bind["b"] == expected_bucket - ), f"rooms={rooms} → bucket {bind['b']} != {expected_bucket}" + assert bind["b"] == expected_bucket, ( + f"rooms={rooms} → bucket {bind['b']} != {expected_bucket}" + ) def test_bucket_keyed_by_area_when_area_known() -> None: diff --git a/tradein-mvp/backend/tests/test_estimator_expected_sold_clamp.py b/tradein-mvp/backend/tests/test_estimator_expected_sold_clamp.py index ca8842ac..8cc771d6 100644 --- a/tradein-mvp/backend/tests/test_estimator_expected_sold_clamp.py +++ b/tradein-mvp/backend/tests/test_estimator_expected_sold_clamp.py @@ -134,13 +134,13 @@ def test_expected_sold_clamped_to_headline_when_ratio_above_1( est = _run_estimate((ratio, "per_rooms"), clamp_enabled=True) assert est.median_price_rub > 0, "headline должен быть задан аналогами" assert est.expected_sold_price_rub is not None - assert ( - est.expected_sold_price_rub <= est.median_price_rub - ), f"expected_sold {est.expected_sold_price_rub} > asking {est.median_price_rub}" + assert est.expected_sold_price_rub <= est.median_price_rub, ( + f"expected_sold {est.expected_sold_price_rub} > asking {est.median_price_rub}" + ) assert est.expected_sold_per_m2 is not None - assert ( - est.expected_sold_per_m2 <= est.median_price_per_m2 - ), "expected_sold_per_m2 превышает median_price_per_m2" + assert est.expected_sold_per_m2 <= est.median_price_per_m2, ( + "expected_sold_per_m2 превышает median_price_per_m2" + ) # #1966: expected_sold range is now a calibrated ~80% PI around the point # (point × [p10, p90] of sold/expected_sold), so it is NO LONGER bounded by the # asking-IQR band — the high arm (point × 1.392) legitimately exceeds range_high. diff --git a/tradein-mvp/backend/tests/test_estimator_floor_optional.py b/tradein-mvp/backend/tests/test_estimator_floor_optional.py index 718cfe79..addcb2d9 100644 --- a/tradein-mvp/backend/tests/test_estimator_floor_optional.py +++ b/tradein-mvp/backend/tests/test_estimator_floor_optional.py @@ -4,6 +4,7 @@ Uses MagicMock for DB (consistent with other test_estimator_* tests — no live The test validates schema construction + that floor=None is accepted without ValueError. The actual NOT NULL guard is covered by migration 065. """ + from __future__ import annotations import os diff --git a/tradein-mvp/backend/tests/test_estimator_headline_sufficiency.py b/tradein-mvp/backend/tests/test_estimator_headline_sufficiency.py index a4033a0f..d3df789a 100644 --- a/tradein-mvp/backend/tests/test_estimator_headline_sufficiency.py +++ b/tradein-mvp/backend/tests/test_estimator_headline_sufficiency.py @@ -298,9 +298,9 @@ def test_repair_coefficient_now_applies_to_thin_sample() -> None: geo=_geo(), dadata_qc_geo=None, ) - assert ( - pr.median_price != pr_no_repair.median_price - ), "repair coefficient must be applied even for a thin (#oblast-E-flagged) sample" + assert pr.median_price != pr_no_repair.median_price, ( + "repair coefficient must be applied even for a thin (#oblast-E-flagged) sample" + ) # ───────────────────────────────────────────────────────────────────────────── diff --git a/tradein-mvp/backend/tests/test_estimator_null_floor_timeout.py b/tradein-mvp/backend/tests/test_estimator_null_floor_timeout.py index fecc2c63..95ded602 100644 --- a/tradein-mvp/backend/tests/test_estimator_null_floor_timeout.py +++ b/tradein-mvp/backend/tests/test_estimator_null_floor_timeout.py @@ -77,18 +77,14 @@ def test_estimate_null_floor_skips_imv_and_cian() -> None: async def _run() -> None: with ( - patch("app.services.estimator.geocode", - new=AsyncMock(return_value=_make_fake_geo())), - patch("app.services.estimator.get_house_metadata", - new=AsyncMock(return_value=None)), + patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_fake_geo())), + patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)), patch("app.services.estimator._fetch_analogs", return_value=([], False, "W")), patch("app.services.estimator._fetch_deals", return_value=[]), patch("app.services.estimator._get_or_fetch_imv_cached", new=imv_mock), - patch("app.services.estimator._get_or_fetch_yandex_valuation_cached", - new=yandex_mock), + patch("app.services.estimator._get_or_fetch_yandex_valuation_cached", new=yandex_mock), patch("app.services.estimator.estimate_via_cian_valuation", new=cian_mock), - patch("app.services.estimator._get_asking_sold_ratio", - return_value=(None, None)), + patch("app.services.estimator._get_asking_sold_ratio", return_value=(None, None)), ): result = await estimate_quality(payload, db) @@ -120,18 +116,14 @@ def test_estimate_full_floor_awaits_imv_and_cian() -> None: async def _run() -> None: with ( - patch("app.services.estimator.geocode", - new=AsyncMock(return_value=_make_fake_geo())), - patch("app.services.estimator.get_house_metadata", - new=AsyncMock(return_value=None)), + patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_fake_geo())), + patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)), patch("app.services.estimator._fetch_analogs", return_value=([], False, "W")), patch("app.services.estimator._fetch_deals", return_value=[]), patch("app.services.estimator._get_or_fetch_imv_cached", new=imv_mock), - patch("app.services.estimator._get_or_fetch_yandex_valuation_cached", - new=yandex_mock), + patch("app.services.estimator._get_or_fetch_yandex_valuation_cached", new=yandex_mock), patch("app.services.estimator.estimate_via_cian_valuation", new=cian_mock), - patch("app.services.estimator._get_asking_sold_ratio", - return_value=(None, None)), + patch("app.services.estimator._get_asking_sold_ratio", return_value=(None, None)), ): result = await estimate_quality(payload, db) @@ -161,20 +153,19 @@ def test_estimate_yandex_timeout_degrades_no_5xx() -> None: async def _run() -> None: with ( - patch("app.services.estimator.geocode", - new=AsyncMock(return_value=_make_fake_geo())), - patch("app.services.estimator.get_house_metadata", - new=AsyncMock(return_value=None)), + patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_fake_geo())), + patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)), patch("app.services.estimator._fetch_analogs", return_value=([], False, "W")), patch("app.services.estimator._fetch_deals", return_value=[]), - patch("app.services.estimator._get_or_fetch_imv_cached", - new=AsyncMock(return_value=None)), - patch("app.services.estimator._get_or_fetch_yandex_valuation_cached", - new=yandex_mock), - patch("app.services.estimator.estimate_via_cian_valuation", - new=AsyncMock(return_value=None)), - patch("app.services.estimator._get_asking_sold_ratio", - return_value=(None, None)), + patch( + "app.services.estimator._get_or_fetch_imv_cached", new=AsyncMock(return_value=None) + ), + patch("app.services.estimator._get_or_fetch_yandex_valuation_cached", new=yandex_mock), + patch( + "app.services.estimator.estimate_via_cian_valuation", + new=AsyncMock(return_value=None), + ), + patch("app.services.estimator._get_asking_sold_ratio", return_value=(None, None)), ): result = await estimate_quality(payload, db) @@ -203,26 +194,23 @@ def test_estimate_yandex_httpx_timeout_at_scraper_degrades_no_5xx() -> None: fake_scraper = MagicMock() fake_scraper.__aenter__ = AsyncMock(return_value=fake_scraper) fake_scraper.__aexit__ = AsyncMock(return_value=False) - fake_scraper.fetch_house_history = AsyncMock( - side_effect=httpx.TimeoutException("read timeout") - ) + fake_scraper.fetch_house_history = AsyncMock(side_effect=httpx.TimeoutException("read timeout")) async def _run() -> None: with ( - patch("app.services.estimator.geocode", - new=AsyncMock(return_value=_make_fake_geo())), - patch("app.services.estimator.get_house_metadata", - new=AsyncMock(return_value=None)), + patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_fake_geo())), + patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)), patch("app.services.estimator._fetch_analogs", return_value=([], False, "W")), patch("app.services.estimator._fetch_deals", return_value=[]), - patch("app.services.estimator._get_or_fetch_imv_cached", - new=AsyncMock(return_value=None)), - patch("app.services.estimator.YandexValuationScraper", - return_value=fake_scraper), - patch("app.services.estimator.estimate_via_cian_valuation", - new=AsyncMock(return_value=None)), - patch("app.services.estimator._get_asking_sold_ratio", - return_value=(None, None)), + patch( + "app.services.estimator._get_or_fetch_imv_cached", new=AsyncMock(return_value=None) + ), + patch("app.services.estimator.YandexValuationScraper", return_value=fake_scraper), + patch( + "app.services.estimator.estimate_via_cian_valuation", + new=AsyncMock(return_value=None), + ), + patch("app.services.estimator._get_asking_sold_ratio", return_value=(None, None)), ): result = await estimate_quality(payload, db) diff --git a/tradein-mvp/backend/tests/test_estimator_radius_floor.py b/tradein-mvp/backend/tests/test_estimator_radius_floor.py index 1551978e..aba07b00 100644 --- a/tradein-mvp/backend/tests/test_estimator_radius_floor.py +++ b/tradein-mvp/backend/tests/test_estimator_radius_floor.py @@ -135,9 +135,9 @@ def test_radius_median_below_dkp_floor_is_lifted() -> None: est = _run_estimate(analogs, dkp_raw, radius_floor_factor=0.8) floor_ppm2 = 150_000 * 0.8 # 120_000 - assert ( - est.median_price_per_m2 >= floor_ppm2 - ), f"median_ppm2={est.median_price_per_m2} должна быть >= floor={floor_ppm2}" + assert est.median_price_per_m2 >= floor_ppm2, ( + f"median_ppm2={est.median_price_per_m2} должна быть >= floor={floor_ppm2}" + ) # ── тест 2: radius median выше floor → no-op ───────────────────────────────── @@ -164,9 +164,9 @@ def test_radius_median_above_dkp_floor_unchanged() -> None: floor_ppm2 = 150_000 * 0.8 # 120_000 assert est.median_price_per_m2 > floor_ppm2, "median должна быть выше floor (no-op)" # Медиана соответствует аналогам (~200k), а не floor - assert ( - 180_000 <= est.median_price_per_m2 <= 220_000 - ), f"median_ppm2={est.median_price_per_m2} должна остаться в диапазоне аналогов (no-op)" + assert 180_000 <= est.median_price_per_m2 <= 220_000, ( + f"median_ppm2={est.median_price_per_m2} должна остаться в диапазоне аналогов (no-op)" + ) # ── тест 3: dkp_raw is None → no-op ───────────────────────────────────────── @@ -182,6 +182,6 @@ def test_no_dkp_raw_no_floor() -> None: est = _run_estimate(analogs, dkp_raw=None, radius_floor_factor=0.8) # median ~80k, без dkp_raw floor не поднимает - assert ( - est.median_price_per_m2 < 100_000 - ), f"median_ppm2={est.median_price_per_m2} без dkp_raw не должна расти" + assert est.median_price_per_m2 < 100_000, ( + f"median_ppm2={est.median_price_per_m2} без dkp_raw не должна расти" + ) diff --git a/tradein-mvp/backend/tests/test_estimator_ratio_tier_fix.py b/tradein-mvp/backend/tests/test_estimator_ratio_tier_fix.py index 0e9dab86..2a717175 100644 --- a/tradein-mvp/backend/tests/test_estimator_ratio_tier_fix.py +++ b/tradein-mvp/backend/tests/test_estimator_ratio_tier_fix.py @@ -150,9 +150,9 @@ def _run_estimate_with_ratio_spy( def test_ratio_called_exactly_once() -> None: """_get_asking_sold_ratio должен вызываться ровно один раз — после headline.""" _est, calls = _run_estimate_with_ratio_spy(_ANALOGS_LOW, (0.80, "per_rooms")) - assert ( - len(calls) == 1 - ), f"_get_asking_sold_ratio должен вызываться 1 раз, вызван {len(calls)} раз" + assert len(calls) == 1, ( + f"_get_asking_sold_ratio должен вызываться 1 раз, вызван {len(calls)} раз" + ) # ── тест 2: anchor поднял headline → ratio вызван с финальным (high) ppm² ──── @@ -298,9 +298,9 @@ def test_ratio_tier_uses_final_headline_after_anchor() -> None: ppm2_used = captured_anchor_ppm2[0] assert ppm2_used is not None # Финальный headline должен быть в зоне anchor (~300k), а НЕ в зоне radius (~105k). - assert ( - ppm2_used > 200_000 - ), f"ratio должен вызываться с anchor ppm2 (~300k), получено {ppm2_used}" + assert ppm2_used > 200_000, ( + f"ratio должен вызываться с anchor ppm2 (~300k), получено {ppm2_used}" + ) # ── тест 3: graceful — нет ratio → expected_sold_* = None, headline не изменён ── diff --git a/tradein-mvp/backend/tests/test_estimator_repair_coef.py b/tradein-mvp/backend/tests/test_estimator_repair_coef.py index 83393181..886c08a9 100644 --- a/tradein-mvp/backend/tests/test_estimator_repair_coef.py +++ b/tradein-mvp/backend/tests/test_estimator_repair_coef.py @@ -142,9 +142,9 @@ def test_excellent_to_needs_repair_ratio_matches_coef() -> None: actual_ratio = excellent.median_price_rub / needs_repair.median_price_rub # int() truncation on both medians ⇒ allow a small tolerance. - assert ( - abs(actual_ratio - expected_ratio) < 0.005 - ), f"excellent/needs_repair ratio {actual_ratio:.5f} != coef ratio {expected_ratio:.5f}" + assert abs(actual_ratio - expected_ratio) < 0.005, ( + f"excellent/needs_repair ratio {actual_ratio:.5f} != coef ratio {expected_ratio:.5f}" + ) def test_standard_is_baseline_noop() -> None: diff --git a/tradein-mvp/backend/tests/test_estimator_source_quota.py b/tradein-mvp/backend/tests/test_estimator_source_quota.py index 9b02b85c..a1745b7f 100644 --- a/tradein-mvp/backend/tests/test_estimator_source_quota.py +++ b/tradein-mvp/backend/tests/test_estimator_source_quota.py @@ -172,9 +172,9 @@ def test_source_quota_includes_all_when_supply_below_min() -> None: result, _, _ = _fetch_analogs(db, lat=56.838, lon=60.595, rooms=1, area=38.0, radius_m=1000) cian_count = sum(1 for r in result if r["source"] == "cian") - assert ( - cian_count == 3 - ), f"All 3 Cian listings (below MIN quota) must be included, got {cian_count}" + assert cian_count == 3, ( + f"All 3 Cian listings (below MIN quota) must be included, got {cian_count}" + ) assert len(result) == 8 # 5 avito + 3 cian diff --git a/tradein-mvp/backend/tests/test_extract_short_addr.py b/tradein-mvp/backend/tests/test_extract_short_addr.py index eb8b2b58..198a1810 100644 --- a/tradein-mvp/backend/tests/test_extract_short_addr.py +++ b/tradein-mvp/backend/tests/test_extract_short_addr.py @@ -8,15 +8,14 @@ from app.services.estimator import _extract_short_addr def test_full_admin_chain() -> None: - assert _extract_short_addr( - "Свердловская область, г. Екатеринбург, Склад, ул. Заводская, д. 44-а" - ) == "ул. Заводская, д. 44-а" + assert ( + _extract_short_addr("Свердловская область, г. Екатеринбург, Склад, ул. Заводская, д. 44-а") + == "ул. Заводская, д. 44-а" + ) def test_russia_prefix() -> None: - assert _extract_short_addr( - "Россия, Екатеринбург, ул. Малышева, 1" - ) == "ул. Малышева, 1" + assert _extract_short_addr("Россия, Екатеринбург, ул. Малышева, 1") == "ул. Малышева, 1" def test_apt_stripped() -> None: diff --git a/tradein-mvp/backend/tests/test_geo_precision.py b/tradein-mvp/backend/tests/test_geo_precision.py index f0f2da67..bece66b0 100644 --- a/tradein-mvp/backend/tests/test_geo_precision.py +++ b/tradein-mvp/backend/tests/test_geo_precision.py @@ -76,9 +76,9 @@ async def test_geocode_missing_sets_city_precision_for_bare_city_address() -> No # Inspect the UPDATE call parameters: geo_precision must be 'city'. update_call = db.execute.call_args_list[1] update_params = update_call[0][1] # positional arg[1] = params dict - assert ( - update_params.get("precision") == "city" - ), f"Expected precision='city' for bare city address, got {update_params.get('precision')!r}" + assert update_params.get("precision") == "city", ( + f"Expected precision='city' for bare city address, got {update_params.get('precision')!r}" + ) @pytest.mark.asyncio @@ -107,9 +107,9 @@ async def test_geocode_missing_sets_none_precision_for_precise_address() -> None update_call = db.execute.call_args_list[1] update_params = update_call[0][1] - assert ( - update_params.get("precision") is None - ), f"Expected precision=None for precise address, got {update_params.get('precision')!r}" + assert update_params.get("precision") is None, ( + f"Expected precision=None for precise address, got {update_params.get('precision')!r}" + ) @pytest.mark.asyncio @@ -138,9 +138,9 @@ async def test_geocode_missing_sets_city_precision_for_locality_confidence() -> update_call = db.execute.call_args_list[1] update_params = update_call[0][1] - assert ( - update_params.get("precision") == "city" - ), f"Expected precision='city' for locality confidence, got {update_params.get('precision')!r}" + assert update_params.get("precision") == "city", ( + f"Expected precision='city' for locality confidence, got {update_params.get('precision')!r}" + ) # ── (b) estimator radius-analog SQL contains geo_precision exclusion ────────── diff --git a/tradein-mvp/backend/tests/test_house_imv_retry_stuck.py b/tradein-mvp/backend/tests/test_house_imv_retry_stuck.py index b414aefc..f6bd4605 100644 --- a/tradein-mvp/backend/tests/test_house_imv_retry_stuck.py +++ b/tradein-mvp/backend/tests/test_house_imv_retry_stuck.py @@ -203,9 +203,9 @@ async def test_explicit_only_status_still_takes_exhausted_houses() -> None: {"ids": list(_IDS)}, ) } - assert ( - statuses[_H_EXHAUSTED] != "transient_error" - ), "явно запрошенный статус обрабатывается целиком, включая исчерпавшие лимит" + assert statuses[_H_EXHAUSTED] != "transient_error", ( + "явно запрошенный статус обрабатывается целиком, включая исчерпавшие лимит" + ) # Автоповтора поверх явного запроса нет: pending не тронут. assert statuses[_H_PENDING] == "pending" assert result.retried == 0 diff --git a/tradein-mvp/backend/tests/test_matching.py b/tradein-mvp/backend/tests/test_matching.py index e0066101..a1d7e7ef 100644 --- a/tradein-mvp/backend/tests/test_matching.py +++ b/tradein-mvp/backend/tests/test_matching.py @@ -439,13 +439,13 @@ def test_match_house_advisory_lock_called_first(): sql_obj = first_call[0][0] # TextClause bind = first_call[0][1] # dict - assert "pg_advisory_xact_lock" in str( - sql_obj - ), f"first execute must be advisory lock, got: {sql_obj}" + assert "pg_advisory_xact_lock" in str(sql_obj), ( + f"first execute must be advisory lock, got: {sql_obj}" + ) assert "fp" in bind, f"lock bind must include fp, got: {bind}" - assert ( - isinstance(bind["fp"], str) and len(bind["fp"]) == 32 - ), f"fp must be 32-char sha256 hex, got: {bind.get('fp')!r}" + assert isinstance(bind["fp"], str) and len(bind["fp"]) == 32, ( + f"fp must be 32-char sha256 hex, got: {bind.get('fp')!r}" + ) # --------------------------------------------------------------------------- @@ -484,9 +484,9 @@ def test_geo_match_does_not_register_alias(): db, "n1", "ext-geo", address="улица Новая 3", lat=56.83, lon=60.59 ) assert (house_id, conf, method) == (22, 0.7, "geo_proximity") - assert not any( - "INSERT INTO house_address_aliases" in s for s in _executed_sqls(db) - ), "geo match must not write an alias (P3)" + assert not any("INSERT INTO house_address_aliases" in s for s in _executed_sqls(db)), ( + "geo match must not write an alias (P3)" + ) def test_geo_match_rejected_when_house_number_differs(): @@ -539,18 +539,18 @@ def test_bare_street_numberless_no_house_created(): ) assert (house_id, conf, method) == (None, 0.0, "no_house_number") sqls = _executed_sqls(db) - assert not any( - "normalized_address = :na" in s for s in sqls - ), "bare-street address must not run Tier 2b normalized_address lookup (P1)" - assert not any( - "INSERT INTO houses" in s for s in sqls - ), "numberless address must not create a house (P1 extended)" - assert not any( - "INSERT INTO house_address_aliases" in s for s in sqls - ), "numberless address must not register an alias (P1)" - assert not any( - "INSERT INTO house_sources" in s for s in sqls - ), "numberless refusal must not upsert house_sources" + assert not any("normalized_address = :na" in s for s in sqls), ( + "bare-street address must not run Tier 2b normalized_address lookup (P1)" + ) + assert not any("INSERT INTO houses" in s for s in sqls), ( + "numberless address must not create a house (P1 extended)" + ) + assert not any("INSERT INTO house_address_aliases" in s for s in sqls), ( + "numberless address must not register an alias (P1)" + ) + assert not any("INSERT INTO house_sources" in s for s in sqls), ( + "numberless refusal must not upsert house_sources" + ) def test_numberless_none_address_with_coords_no_house_created(): @@ -575,9 +575,9 @@ def test_numberless_none_address_with_coords_no_house_created(): db, "yandex", "ext-none-addr", address=None, lat=56.83, lon=60.59 ) assert (house_id, conf, method) == (None, 0.0, "no_house_number") - assert not any( - "INSERT INTO houses" in s for s in _executed_sqls(db) - ), "address-NULL coords-only listing must not create a house (P1 extended)" + assert not any("INSERT INTO houses" in s for s in _executed_sqls(db)), ( + "address-NULL coords-only listing must not create a house (P1 extended)" + ) def test_numberless_address_with_cadastral_creates_house(): @@ -611,9 +611,9 @@ def test_numberless_address_with_cadastral_creates_house(): building_cadastral_number="66:41:0000000:12345", ) assert (house_id, conf, method) == (321, 1.0, "new") - assert any( - "INSERT INTO houses" in s for s in _executed_sqls(db) - ), "numberless + cadastral must still create a house (cadastral = identity)" + assert any("INSERT INTO houses" in s for s in _executed_sqls(db)), ( + "numberless + cadastral must still create a house (cadastral = identity)" + ) def test_insert_alias_noop_for_bare_street(): @@ -735,9 +735,9 @@ def test_tier2b_no_coords_no_city_token_skips_and_creates_new(): db, "avito", "ext-2b-bare", address="улица Ленина 100" ) assert (house_id, conf, method) == (910, 1.0, "new") - assert not any( - "normalized_address = :na" in s for s in _executed_sqls(db) - ), "bare common-street with no coords/city must not run a Tier-2b lookup" + assert not any("normalized_address = :na" in s for s in _executed_sqls(db)), ( + "bare common-street with no coords/city must not run a Tier-2b lookup" + ) def test_tier2b_no_coords_with_city_token_matches(): @@ -792,12 +792,12 @@ def test_tier2a_coord_less_non_ekb_city_skips_both_lookups_and_creates_new(): ) assert (house_id, conf, method) == (701, 1.0, "new") sqls = _executed_sqls(db) - assert not any( - "fingerprint = :fp" in s for s in sqls - ), "coord-less non-ЕКБ card must NOT run the Tier-2a fingerprint lookup" - assert not any( - "normalized_address = :na" in s for s in sqls - ), "coord-less non-ЕКБ card must NOT run the Tier-2b normalized_address lookup" + assert not any("fingerprint = :fp" in s for s in sqls), ( + "coord-less non-ЕКБ card must NOT run the Tier-2a fingerprint lookup" + ) + assert not any("normalized_address = :na" in s for s in sqls), ( + "coord-less non-ЕКБ card must NOT run the Tier-2b normalized_address lookup" + ) def test_tier2a_coord_less_ekb_city_still_matches(): @@ -818,9 +818,9 @@ def test_tier2a_coord_less_ekb_city_still_matches(): db, "avito", "ext-2a-ekb", address="Екатеринбург, ул. Ленина, 5" ) assert (house_id, conf, method) == (88, 0.9, "fingerprint") - assert any( - "fingerprint = :fp" in s for s in _executed_sqls(db) - ), "ЕКБ coord-less card must still run the Tier-2a fingerprint lookup" + assert any("fingerprint = :fp" in s for s in _executed_sqls(db)), ( + "ЕКБ coord-less card must still run the Tier-2a fingerprint lookup" + ) def test_tier2a_coord_less_bare_street_still_runs_tier2a(): @@ -872,9 +872,9 @@ def test_tier2a_bare_card_from_oblast_sweep_skips_alias_lookups(): ) assert (house_id, conf, method) == (2777, 1.0, "new") sqls = _executed_sqls(db) - assert not any( - "fingerprint = :fp" in s for s in sqls - ), "карточка чужого города прошла Tier-2a по бескоординатному ключу «улица + номер»" + assert not any("fingerprint = :fp" in s for s in sqls), ( + "карточка чужого города прошла Tier-2a по бескоординатному ключу «улица + номер»" + ) assert not any("normalized_address = :na" in s for s in sqls) @@ -1085,13 +1085,13 @@ def test_field_priority_sources_are_lists_or_known_string_rules(): rules alongside list entries. Both are valid per LISTING_FIELD_PRIORITY type hint. """ for col, sources in HOUSE_FIELD_PRIORITY.items(): - assert ( - isinstance(sources, list) or sources in VALID_STRING_RULES - ), f"HOUSE_FIELD_PRIORITY[{col!r}] must be list or known rule, got {sources!r}" + assert isinstance(sources, list) or sources in VALID_STRING_RULES, ( + f"HOUSE_FIELD_PRIORITY[{col!r}] must be list or known rule, got {sources!r}" + ) for col, sources in LISTING_FIELD_PRIORITY.items(): - assert ( - isinstance(sources, list) or sources in VALID_STRING_RULES - ), f"LISTING_FIELD_PRIORITY[{col!r}] must be list or known rule, got {sources!r}" + assert isinstance(sources, list) or sources in VALID_STRING_RULES, ( + f"LISTING_FIELD_PRIORITY[{col!r}] must be list or known rule, got {sources!r}" + ) def test_update_canonical_fields_is_callable(): diff --git a/tradein-mvp/backend/tests/test_matching_tier_reachability_2674.py b/tradein-mvp/backend/tests/test_matching_tier_reachability_2674.py index b16be85b..51107d0c 100644 --- a/tradein-mvp/backend/tests/test_matching_tier_reachability_2674.py +++ b/tradein-mvp/backend/tests/test_matching_tier_reachability_2674.py @@ -65,9 +65,9 @@ def test_fias_tier_is_gone_from_create_path_but_alive_in_readonly() -> None: # Ищем именно литерал method-значения ('"fias_exact"'), а не слово в комментарии: # комментарий-надгробие про удалённый тир остаться должен, ветка — нет. - assert '"fias_exact"' not in inspect.getsource( - match_or_create_house - ), "мёртвая fias-ветка вернулась в путь создания домов" + assert '"fias_exact"' not in inspect.getsource(match_or_create_house), ( + "мёртвая fias-ветка вернулась в путь создания домов" + ) assert '"fias_exact"' in inspect.getsource(match_house_readonly) @@ -104,9 +104,9 @@ def test_house_key_never_accepts_flat_cadastre() -> None: assert "building_cadastral_number" in _params(match_or_create_house) src = inspect.getsource(match_or_create_house) - assert ( - "cad = building_cadastral_number\n" in src - ), "в ключ дома вернулся фолбэк на кадастр квартиры" + assert "cad = building_cadastral_number\n" in src, ( + "в ключ дома вернулся фолбэк на кадастр квартиры" + ) def test_sweep_city_actually_reaches_the_matcher_from_save_listings() -> None: @@ -128,6 +128,6 @@ def test_sweep_city_actually_reaches_the_matcher_from_save_listings() -> None: ) # save_listings считает lot_city (город batch'а после гео-guard'а) и обязан отдать # именно его, а не сырой city-аргумент: лот вне city_radius_km города НЕ помечен. - assert "city=lot_city" in inspect.getsource( - save_listings - ), "save_listings отдаёт матчеру не lot_city — гео-guard соседнего города обойдён" + assert "city=lot_city" in inspect.getsource(save_listings), ( + "save_listings отдаёт матчеру не lot_city — гео-guard соседнего города обойдён" + ) diff --git a/tradein-mvp/backend/tests/test_migration_201_purge_dead_mobileproxy_proxies.py b/tradein-mvp/backend/tests/test_migration_201_purge_dead_mobileproxy_proxies.py index 72500fe3..34af6701 100644 --- a/tradein-mvp/backend/tests/test_migration_201_purge_dead_mobileproxy_proxies.py +++ b/tradein-mvp/backend/tests/test_migration_201_purge_dead_mobileproxy_proxies.py @@ -51,9 +51,9 @@ def test_migration_201_deletes_by_domain_not_id() -> None: flat = _flat(_executable_sql()) assert "delete from scrape_proxies" in flat assert "where url like '%mobileproxy.space%'" in flat - assert ( - re.search(r"where\s+id\s*(=|in)", flat) is None - ), "миграция не должна фильтровать по id — id разъезжается между средами" + assert re.search(r"where\s+id\s*(=|in)", flat) is None, ( + "миграция не должна фильтровать по id — id разъезжается между средами" + ) def test_migration_201_is_idempotent_by_construction() -> None: diff --git a/tradein-mvp/backend/tests/test_migration_262_oblast_city_sweeps_wave2.py b/tradein-mvp/backend/tests/test_migration_262_oblast_city_sweeps_wave2.py index 75da91a6..492fdb1c 100644 --- a/tradein-mvp/backend/tests/test_migration_262_oblast_city_sweeps_wave2.py +++ b/tradein-mvp/backend/tests/test_migration_262_oblast_city_sweeps_wave2.py @@ -300,9 +300,9 @@ def test_mikhaylovsk_has_only_cian_row() -> None: РОВНО одну строку (cian), НЕ три.""" rows = _row_sources(_MIGRATION_262) mikhaylovsk_providers = {p for p, s in rows if s == "mikhaylovsk"} - assert mikhaylovsk_providers == { - "cian" - }, f"mikhaylovsk должен иметь только cian-строку, получено: {mikhaylovsk_providers}" + assert mikhaylovsk_providers == {"cian"}, ( + f"mikhaylovsk должен иметь только cian-строку, получено: {mikhaylovsk_providers}" + ) # ── CITY_ANCHORS parity ───────────────────────────────────────────────────── @@ -332,9 +332,9 @@ def test_city_anchors_has_no_slug_without_schedule_rows() -> None: slug for _p, slug in _row_sources(_MIGRATION_262) } orphaned = set(CITY_ANCHORS) - seeded_slugs - assert ( - not orphaned - ), f"CITY_ANCHORS содержит slug без scrape_schedules-строк: {sorted(orphaned)}" + assert not orphaned, ( + f"CITY_ANCHORS содержит slug без scrape_schedules-строк: {sorted(orphaned)}" + ) def test_city_anchors_wave2_count_and_content() -> None: @@ -398,9 +398,9 @@ def test_migration_262_windows_are_one_hour() -> None: полночь, см. scraper_kit.orchestration.scheduler).""" for provider, slug, start, end in _row_windows(_MIGRATION_262): expected = (start + 1) % 24 - assert ( - end == expected - ), f"{provider}_{slug}: window [{start},{end}) не 1-часовое (ожидали end={expected})" + assert end == expected, ( + f"{provider}_{slug}: window [{start},{end}) не 1-часовое (ожидали end={expected})" + ) def test_migration_262_window_hours_satisfy_db_check_constraint() -> None: diff --git a/tradein-mvp/backend/tests/test_password.py b/tradein-mvp/backend/tests/test_password.py index a03e5c9f..8d1dd9e1 100644 --- a/tradein-mvp/backend/tests/test_password.py +++ b/tradein-mvp/backend/tests/test_password.py @@ -285,9 +285,9 @@ async def test_one_key_cannot_take_more_than_its_share(monkeypatch: pytest.Monke async def _wait_inflight(n: int) -> None: deadline = time.monotonic() + 5 while password_mod._verify_inflight < n: - assert ( - time.monotonic() < deadline - ), f"слотов занято {password_mod._verify_inflight} < {n}" + assert time.monotonic() < deadline, ( + f"слотов занято {password_mod._verify_inflight} < {n}" + ) await asyncio.sleep(0.005) flood = [ diff --git a/tradein-mvp/backend/tests/test_pdf_security.py b/tradein-mvp/backend/tests/test_pdf_security.py index 7d6d21ed..7789b8e0 100644 --- a/tradein-mvp/backend/tests/test_pdf_security.py +++ b/tradein-mvp/backend/tests/test_pdf_security.py @@ -522,9 +522,9 @@ def test_brand_not_taken_from_query_param_docstring() -> None: sig = inspect.signature(estimate_pdf) param_names = list(sig.parameters.keys()) - assert ( - "brand" not in param_names - ), "estimate_pdf should NOT have a 'brand' query param after #7 fix" + assert "brand" not in param_names, ( + "estimate_pdf should NOT have a 'brand' query param after #7 fix" + ) # ── PR-D1: retain_until (paid retention) — cover row + valid_until unaffected ── diff --git a/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py b/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py index a49f1a96..f539f67d 100644 --- a/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py +++ b/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py @@ -435,7 +435,7 @@ def test_real_purge_deletes_only_anonymous_expired_estimates() -> None: remaining_ids = { str(r) for r in db.execute( - _t("SELECT id FROM trade_in_estimates " "WHERE id = ANY(CAST(:ids AS uuid[]))"), + _t("SELECT id FROM trade_in_estimates WHERE id = ANY(CAST(:ids AS uuid[]))"), {"ids": [str(anon_id), str(pilot_id)]}, ) .scalars() @@ -497,9 +497,9 @@ def test_real_preflight_ignores_healthy_paid_row_flags_only_anomaly() -> None: {"order_id": healthy_order, "id": str(healthy_id)}, ) db.commit() - assert ( - task_mod._preflight_paid_candidates(db) == baseline - ), "healthy paid row (retain_until set) must NOT raise the pre-flight count" + assert task_mod._preflight_paid_candidates(db) == baseline, ( + "healthy paid row (retain_until set) must NOT raise the pre-flight count" + ) # Anomaly: retain_until NULL despite a payments row existing -- exactly # the case the two DELETE safeguards exist for. Must raise by exactly one. diff --git a/tradein-mvp/backend/tests/test_ratelimit.py b/tradein-mvp/backend/tests/test_ratelimit.py index b9b71c1a..fdb75f74 100644 --- a/tradein-mvp/backend/tests/test_ratelimit.py +++ b/tradein-mvp/backend/tests/test_ratelimit.py @@ -183,9 +183,9 @@ def test_notify_path_bypasses_general_limiter_400_requests_zero_429(notify_clien statuses = [ notify_client.post("/api/v1/trade-in/payments/notify").status_code for _ in range(400) ] - assert all( - code == 200 for code in statuses - ), f"notify получил 429 хотя бы раз: {[c for c in statuses if c != 200]}" + assert all(code == 200 for code in statuses), ( + f"notify получил 429 хотя бы раз: {[c for c in statuses if c != 200]}" + ) def test_general_limiter_still_active_for_other_paths(notify_client): diff --git a/tradein-mvp/backend/tests/test_sber_index.py b/tradein-mvp/backend/tests/test_sber_index.py index 61b7f1c7..ae70ad4f 100644 --- a/tradein-mvp/backend/tests/test_sber_index.py +++ b/tradein-mvp/backend/tests/test_sber_index.py @@ -203,8 +203,7 @@ def test_active_dashboards_match_captured_secondary_series() -> None: d = _DASH[slug] encoded = build_sber_route(d.slug, "643", d.extra_filter) assert encoded in captured_b64, ( - f"active dashboard {slug!r} route not in captured-live set — " - f"filter={d.extra_filter}" + f"active dashboard {slug!r} route not in captured-live set — filter={d.extra_filter}" ) @@ -345,15 +344,15 @@ def test_decode_sber_response_real_fixture() -> None: # Verify region label decoded correctly regions = {r["ref_area"] for r in rows} - assert ( - "Свердловская область" in regions - ), f"Expected 'Свердловская область' in ref_area values, got: {regions}" + assert "Свердловская область" in regions, ( + f"Expected 'Свердловская область' in ref_area values, got: {regions}" + ) # Verify secondary-market segment present realty_vals = {r["realty"] for r in rows} - assert any( - "тори" in str(v) or "Вторичн" in str(v) for v in realty_vals - ), f"Expected secondary-market label in realty field, got: {realty_vals}" + assert any("тори" in str(v) or "Вторичн" in str(v) for v in realty_vals), ( + f"Expected secondary-market label in realty field, got: {realty_vals}" + ) # --------------------------------------------------------------------------- @@ -434,9 +433,9 @@ async def test_pull_sber_indices_upsert_on_conflict_idempotent() -> None: # Must NOT contain :: type casts (psycopg v3 rule) import re - assert not re.search( - r":[a-z_]+::[a-z]", upsert_sql - ), "SQL must not contain ::type casts — use CAST(... AS type) instead" + assert not re.search(r":[a-z_]+::[a-z]", upsert_sql), ( + "SQL must not contain ::type casts — use CAST(... AS type) instead" + ) @pytest.mark.asyncio @@ -532,9 +531,9 @@ async def test_pull_sber_indices_asking_benchmark_logged(caplog: pytest.LogCaptu dashboards=[_DASH["dinamika-tsen-obyavlenii"]], ) - assert any( - "benchmark" in record.message for record in caplog.records - ), "Expected a benchmark log line after upserting dinamika-tsen-obyavlenii" - assert any( - "115000" in record.message for record in caplog.records - ), "Benchmark log should include the latest asking index value" + assert any("benchmark" in record.message for record in caplog.records), ( + "Expected a benchmark log line after upserting dinamika-tsen-obyavlenii" + ) + assert any("115000" in record.message for record in caplog.records), ( + "Benchmark log should include the latest asking index value" + ) diff --git a/tradein-mvp/backend/tests/test_scraper_admin_apis.py b/tradein-mvp/backend/tests/test_scraper_admin_apis.py index 813d78a2..ea7f0b5d 100644 --- a/tradein-mvp/backend/tests/test_scraper_admin_apis.py +++ b/tradein-mvp/backend/tests/test_scraper_admin_apis.py @@ -477,9 +477,9 @@ def test_data_quality_pct_in_range(client: TestClient) -> None: for src in body["sources"]: for field_name, pct in src["fields"].items(): - assert ( - 0.0 <= pct <= 100.0 - ), f"source={src['source']} field={field_name} pct={pct} вне [0,100]" + assert 0.0 <= pct <= 100.0, ( + f"source={src['source']} field={field_name} pct={pct} вне [0,100]" + ) # ── Security: текст исключения не утекает в HTTP-ответ (#2234) ──────────────── diff --git a/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py b/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py index b973e19a..781dd7c9 100644 --- a/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py +++ b/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py @@ -164,9 +164,9 @@ def test_real_build_product_handlers_covers_all_scheduled_sources() -> None: real_registry = build_registry(build_product_handlers(ctx=None)) # type: ignore[arg-type] for source in _PRODUCT_SOURCES | _KIT_NATIVE_SOURCES: - assert ( - resolve_handler(source, real_registry) is not None - ), f"real build_product_handlers()/build_registry() misses source={source}" + assert resolve_handler(source, real_registry) is not None, ( + f"real build_product_handlers()/build_registry() misses source={source}" + ) def test_kit_native_handler_set() -> None: diff --git a/tradein-mvp/backend/tests/test_segment_guard_1186.py b/tradein-mvp/backend/tests/test_segment_guard_1186.py index 289ff36d..78f7a465 100644 --- a/tradein-mvp/backend/tests/test_segment_guard_1186.py +++ b/tradein-mvp/backend/tests/test_segment_guard_1186.py @@ -40,9 +40,9 @@ def _norm(s: str) -> str: def test_common_where_has_canonical_guard() -> None: """_COMMON_WHERE используется Tier S и Tier H — должен содержать канон-предикат.""" - assert _GUARD_RE.search( - est_mod._COMMON_WHERE - ), "_COMMON_WHERE lacks novostroyki guard — Tier S/H comp set contaminated" + assert _GUARD_RE.search(est_mod._COMMON_WHERE), ( + "_COMMON_WHERE lacks novostroyki guard — Tier S/H comp set contaminated" + ) def test_common_where_no_old_neq_form() -> None: @@ -95,18 +95,18 @@ _REDERIVE_SQL_TEXT = str(ratio_mod._REDERIVE_SQL.text) def test_ask_side_cte_has_guard() -> None: """ask_side CTE в _REDERIVE_SQL (per-rooms asking медиана) — guard обязателен.""" - assert _GUARD_RE.search( - _REDERIVE_SQL_TEXT - ), "ask_side CTE in _REDERIVE_SQL lacks novostroyki guard" + assert _GUARD_RE.search(_REDERIVE_SQL_TEXT), ( + "ask_side CTE in _REDERIVE_SQL lacks novostroyki guard" + ) def test_ask_global_cte_has_guard() -> None: """ask_global CTE в _REDERIVE_SQL (global fallback asking медиана) — guard обязателен.""" # _REDERIVE_SQL содержит два `ask_global`-блока; ищем оба через count. matches = len(_GUARD_RE.findall(_REDERIVE_SQL_TEXT)) - assert ( - matches >= 2 - ), f"Expected ≥2 guard occurrences in _REDERIVE_SQL (ask_side + ask_global), got {matches}" + assert matches >= 2, ( + f"Expected ≥2 guard occurrences in _REDERIVE_SQL (ask_side + ask_global), got {matches}" + ) # ── Поведенческие тесты: _fetch_analogs (mock DB) ──────────────────────────── @@ -164,9 +164,9 @@ def test_fetch_analogs_sql_guard_present_novostroyki_excluded() -> None: """ src = inspect.getsource(est_mod._fetch_analogs) # Guard должен присутствовать хотя бы один раз в теле функции. - assert _GUARD_RE.search( - src - ), "_fetch_analogs SQL no longer contains novostroyki guard — guard was removed!" + assert _GUARD_RE.search(src), ( + "_fetch_analogs SQL no longer contains novostroyki guard — guard was removed!" + ) def test_null_segment_listing_not_excluded_by_guard() -> None: @@ -185,9 +185,9 @@ def test_null_segment_listing_not_excluded_by_guard() -> None: ) # NULL-segment listing должен присутствовать в результате (не отброшен Python-стороной). - assert any( - r.get("source") == "avito" for r in result - ), "NULL-segment listing was unexpectedly excluded from comp set" + assert any(r.get("source") == "avito" for r in result), ( + "NULL-segment listing was unexpectedly excluded from comp set" + ) def test_vtorichka_segment_listing_included() -> None: @@ -234,12 +234,12 @@ def test_no_neq_novostroyki_form_in_estimator() -> None: Канон: IS NULL OR = 'vtorichka'. """ src = _estimator_full_src() - assert ( - "<> 'novostroyki'" not in src - ), "estimator.py contains deprecated `<> 'novostroyki'` form — use canonical guard" - assert ( - "!= 'novostroyki'" not in src - ), "estimator.py contains deprecated `!= 'novostroyki'` form — use canonical guard" + assert "<> 'novostroyki'" not in src, ( + "estimator.py contains deprecated `<> 'novostroyki'` form — use canonical guard" + ) + assert "!= 'novostroyki'" not in src, ( + "estimator.py contains deprecated `!= 'novostroyki'` form — use canonical guard" + ) def test_anchor_comps_no_dead_listing_segment_param() -> None: @@ -269,6 +269,6 @@ def test_fetch_anchor_comps_tier_c_canonical_guard_not_parametric() -> None: "must be hardcoded canonical guard" ) # И канон-guard на месте (Tier C-блок). - assert _GUARD_RE.search( - src - ), "_fetch_anchor_comps lacks canonical guard after removing parametric form" + assert _GUARD_RE.search(src), ( + "_fetch_anchor_comps lacks canonical guard after removing parametric form" + ) diff --git a/tradein-mvp/backend/tests/test_snapshot_writer.py b/tradein-mvp/backend/tests/test_snapshot_writer.py index a779c98d..6cde5c51 100644 --- a/tradein-mvp/backend/tests/test_snapshot_writer.py +++ b/tradein-mvp/backend/tests/test_snapshot_writer.py @@ -303,9 +303,9 @@ def test_save_detail_enrichment_oph_on_conflict_constraint(): sqls = _get_all_sqls(db) oph_sqls = [s for s in sqls if "offer_price_history" in s] assert oph_sqls, "INSERT offer_price_history не найден" - assert ( - "offer_price_history_listing_change_uq" in oph_sqls[0] - ), "ON CONFLICT должен ссылаться на UNIQUE constraint" + assert "offer_price_history_listing_change_uq" in oph_sqls[0], ( + "ON CONFLICT должен ссылаться на UNIQUE constraint" + ) def test_save_detail_enrichment_skips_price_change_without_change_time(): diff --git a/tradein-mvp/backend/tests/test_street_deals_endpoint.py b/tradein-mvp/backend/tests/test_street_deals_endpoint.py index 003883e1..570711db 100644 --- a/tradein-mvp/backend/tests/test_street_deals_endpoint.py +++ b/tradein-mvp/backend/tests/test_street_deals_endpoint.py @@ -317,15 +317,15 @@ def test_street_regex_word_boundary_no_false_positive() -> None: # Must NOT match — 'мира' appears inside 'Макарова' substring check skipped # but more critically — 'мира' appears inside 'Адмирала Макарова' false_positive_addr = "улица Адмирала Макарова" - assert not re.search( - pattern, false_positive_addr, re.IGNORECASE - ), f"Pattern {pattern!r} should NOT match {false_positive_addr!r}" + assert not re.search(pattern, false_positive_addr, re.IGNORECASE), ( + f"Pattern {pattern!r} should NOT match {false_positive_addr!r}" + ) # Must match — exact word true_positive_addr = "улица Мира 5" - assert re.search( - pattern, true_positive_addr, re.IGNORECASE - ), f"Pattern {pattern!r} should match {true_positive_addr!r}" + assert re.search(pattern, true_positive_addr, re.IGNORECASE), ( + f"Pattern {pattern!r} should match {true_positive_addr!r}" + ) def test_street_regex_param_passed_to_db(trade_in_app: FastAPI) -> None: @@ -355,6 +355,6 @@ def test_street_regex_param_passed_to_db(trade_in_app: FastAPI) -> None: assert "street_regex" in params, f"street_regex not in params: {params}" regex_val = params["street_regex"] # Must contain word-boundary anchors - assert ( - r"\m" in regex_val or r"\b" in regex_val or regex_val.startswith(r"\m") - ), f"Expected word-boundary in regex, got: {regex_val!r}" + assert r"\m" in regex_val or r"\b" in regex_val or regex_val.startswith(r"\m"), ( + f"Expected word-boundary in regex, got: {regex_val!r}" + ) diff --git a/tradein-mvp/backend/tests/test_yandex_city_sweep.py b/tradein-mvp/backend/tests/test_yandex_city_sweep.py index b50729a9..fb3f895a 100644 --- a/tradein-mvp/backend/tests/test_yandex_city_sweep.py +++ b/tradein-mvp/backend/tests/test_yandex_city_sweep.py @@ -219,6 +219,6 @@ def test_combos_sweep_timeout_substantially_larger_than_anchor_timeout() -> None f"combos-mode will still timeout mid-sweep" ) # И намного > ANCHOR_TIMEOUT_SEC (240s) - assert ( - sweep_timeout > ANCHOR_TIMEOUT_SEC * 4 - ), f"sweep_timeout={sweep_timeout:.0f}s should be >> ANCHOR_TIMEOUT_SEC={ANCHOR_TIMEOUT_SEC}s" + assert sweep_timeout > ANCHOR_TIMEOUT_SEC * 4, ( + f"sweep_timeout={sweep_timeout:.0f}s should be >> ANCHOR_TIMEOUT_SEC={ANCHOR_TIMEOUT_SEC}s" + ) diff --git a/tradein-mvp/backend/tests/test_yandex_valuation.py b/tradein-mvp/backend/tests/test_yandex_valuation.py index c6196e84..638c5042 100644 --- a/tradein-mvp/backend/tests/test_yandex_valuation.py +++ b/tradein-mvp/backend/tests/test_yandex_valuation.py @@ -416,9 +416,9 @@ def test_area_regex_rejects_year_concat(): item = YandexValuationScraper._parse_item_text(text) # The "202452,2" token has digits jammed before it (no separator), so the # tightened regex must NOT match it. - assert ( - item is None or item.area_m2 is None - ), f"expected no area match for concat token, got {item.area_m2 if item else 'None item'}" + assert item is None or item.area_m2 is None, ( + f"expected no area match for concat token, got {item.area_m2 if item else 'None item'}" + ) def test_area_regex_accepts_isolated_token(): @@ -474,9 +474,9 @@ def test_area_regex_two_komn_chunk(): text = "8 000 000 ₽ за м²2-комнатная 52,2 м² 5 этаж 10.05.2024 8 000 000 ₽ В продаже" item = YandexValuationScraper._parse_item_text(text) assert item is not None - assert ( - item.area_m2 == 52.2 - ), f"2-комн area must parse despite preceding tokens, got {item.area_m2}" + assert item.area_m2 == 52.2, ( + f"2-комн area must parse despite preceding tokens, got {item.area_m2}" + ) def test_area_regex_still_blocks_year_concat():