Compare commits
No commits in common. "298b43e39950f1eeac19d51d91143c9c78da1a4b" and "61760282375d9b0636499815bd326646c1abcc36" have entirely different histories.
298b43e399
...
6176028237
5 changed files with 4 additions and 100 deletions
|
|
@ -1962,9 +1962,7 @@ def _parse_proxy_host_port(proxy_url: str | None) -> tuple[str | None, int | Non
|
||||||
def list_scrape_runs_unified(
|
def list_scrape_runs_unified(
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db)],
|
||||||
source: Annotated[str | None, Query()] = None,
|
source: Annotated[str | None, Query()] = None,
|
||||||
status: Annotated[
|
status: Annotated[str | None, Query()] = None,
|
||||||
Literal["done", "running", "banned", "zombie", "failed", "cancelled"] | None, Query()
|
|
||||||
] = None,
|
|
||||||
limit: Annotated[int, Query(ge=1, le=200)] = 50,
|
limit: Annotated[int, Query(ge=1, le=200)] = 50,
|
||||||
offset: Annotated[int, Query(ge=0)] = 0,
|
offset: Annotated[int, Query(ge=0)] = 0,
|
||||||
) -> UnifiedScrapeRunsResponse:
|
) -> UnifiedScrapeRunsResponse:
|
||||||
|
|
|
||||||
|
|
@ -1252,24 +1252,7 @@ def save_house_catalog_enrichment(
|
||||||
Возвращает:
|
Возвращает:
|
||||||
{'house_id': N, 'reviews': N, 'sellers': N, 'listings_linked': N, 'placement_history': N}
|
{'house_id': N, 'reviews': N, 'sellers': N, 'listings_linked': N, 'placement_history': N}
|
||||||
"""
|
"""
|
||||||
# 1. Upsert дом с правильным URL из enrichment.
|
# 1. Upsert дом с правильным URL из enrichment
|
||||||
# Гард #1820: id-less graceful-skip новостройки (ext_id=0, нет адреса/координат) —
|
|
||||||
# НЕ персистим canonical house, иначе все безымянные URL склеиваются в один bogus
|
|
||||||
# avito:0 через source_exact-tier и плодят мусорную house_sources("avito","0").
|
|
||||||
if not e.house.ext_id and not (e.house.short_address or e.house.full_address):
|
|
||||||
logger.warning(
|
|
||||||
"save_house_catalog_enrichment: пропуск id-less новостройки без адреса"
|
|
||||||
" (ext_id=%s url=%s) — не создаём bogus avito:0 в house_sources",
|
|
||||||
e.house.ext_id,
|
|
||||||
e.house_url,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"house_id": 0,
|
|
||||||
"reviews": 0,
|
|
||||||
"sellers": 0,
|
|
||||||
"listings_linked": 0,
|
|
||||||
"placement_history": 0,
|
|
||||||
}
|
|
||||||
house_id = _upsert_house_with_url(db, e.house, e.house_url)
|
house_id = _upsert_house_with_url(db, e.house, e.house_url)
|
||||||
|
|
||||||
# 2. Сохранить отзывы
|
# 2. Сохранить отзывы
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ dependencies = [
|
||||||
"Pillow>=10.3.0", # photo sanitization re-encode (#6 audit fix)
|
"Pillow>=10.3.0", # photo sanitization re-encode (#6 audit fix)
|
||||||
"weasyprint>=62.0",
|
"weasyprint>=62.0",
|
||||||
"jinja2>=3.1.0",
|
"jinja2>=3.1.0",
|
||||||
"httpx[socks]>=0.27.0", # для geocoder + scrapers; [socks] тянет socksio для socks5://
|
"httpx>=0.27.0", # для geocoder + scrapers
|
||||||
"tenacity>=9.0.0", # retry с exp backoff
|
"tenacity>=9.0.0", # retry с exp backoff
|
||||||
"selectolax>=0.3.0", # быстрый HTML парсинг для scrapers
|
"selectolax>=0.3.0", # быстрый HTML парсинг для scrapers
|
||||||
"segno>=1.6.0", # QR-код для PDF shareable URL
|
"segno>=1.6.0", # QR-код для PDF shareable URL
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
2. Enrichment-поля (rating_score, developer_name) реально записываются в UPDATE.
|
2. Enrichment-поля (rating_score, developer_name) реально записываются в UPDATE.
|
||||||
3. _persist_house не вызывает db.commit().
|
3. _persist_house не вызывает db.commit().
|
||||||
4. upsert_house и _upsert_house_with_url делегируют в _persist_house.
|
4. upsert_house и _upsert_house_with_url делегируют в _persist_house.
|
||||||
5. #1820: два id-less URL (ext_id=0, без адреса) НЕ создают house_sources(avito,"0").
|
|
||||||
|
|
||||||
Использует тот же паттерн mock-DB что в test_matching.py и test_same_building_match.py.
|
Использует тот же паттерн mock-DB что в test_matching.py и test_same_building_match.py.
|
||||||
"""
|
"""
|
||||||
|
|
@ -14,16 +13,14 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db")
|
||||||
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from app.services.scrapers.avito_houses import (
|
from app.services.scrapers.avito_houses import (
|
||||||
HouseCatalogEnrichment,
|
|
||||||
HouseInfo,
|
HouseInfo,
|
||||||
_persist_house,
|
_persist_house,
|
||||||
_upsert_house_with_url,
|
_upsert_house_with_url,
|
||||||
save_house_catalog_enrichment,
|
|
||||||
upsert_house,
|
upsert_house,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -272,62 +269,3 @@ def test_persist_house_coalesce_fields_in_bind():
|
||||||
assert bind["lat"] == h.lat
|
assert bind["lat"] == h.lat
|
||||||
assert bind["lon"] == h.lon
|
assert bind["lon"] == h.lon
|
||||||
assert bind["year_built"] == h.year_built
|
assert bind["year_built"] == h.year_built
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Test 8: #1820 — ext_id=0 graceful-skip гард
|
|
||||||
# Два id-less URL (ext_id=0, без адреса) → match_or_create_house НЕ вызывается,
|
|
||||||
# house_sources("avito","0") НЕ создаётся, возвращается sentinel dict с house_id=0.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_house_catalog_enrichment_skips_zero_ext_id_no_address():
|
|
||||||
"""#1820: ext_id=0 + нет адреса → гард пропускает персистирование canonical house."""
|
|
||||||
db = MagicMock()
|
|
||||||
|
|
||||||
# Создаём два id-less graceful-skip enrichment (как _parse_development_or_skip возвращает)
|
|
||||||
enrichment_a = HouseCatalogEnrichment(
|
|
||||||
house_url="https://www.avito.ru/catalog/novostroyki/dom-A/",
|
|
||||||
house=HouseInfo(ext_id=0),
|
|
||||||
)
|
|
||||||
enrichment_b = HouseCatalogEnrichment(
|
|
||||||
house_url="https://www.avito.ru/catalog/novostroyki/dom-B/",
|
|
||||||
house=HouseInfo(ext_id=0),
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"app.services.scrapers.avito_houses.match_or_create_house",
|
|
||||||
) as mock_match:
|
|
||||||
result_a = save_house_catalog_enrichment(db, enrichment_a)
|
|
||||||
result_b = save_house_catalog_enrichment(db, enrichment_b)
|
|
||||||
|
|
||||||
# match_or_create_house НЕ должен вызываться — гард сработал до него
|
|
||||||
mock_match.assert_not_called()
|
|
||||||
|
|
||||||
# Возвращается sentinel dict с house_id=0, не плодятся дубли
|
|
||||||
assert result_a["house_id"] == 0, f"ожидали house_id=0, получили {result_a['house_id']}"
|
|
||||||
assert result_b["house_id"] == 0, f"ожидали house_id=0, получили {result_b['house_id']}"
|
|
||||||
|
|
||||||
# db.commit() НЕ вызывается (нет реальной транзакции)
|
|
||||||
db.commit.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_house_catalog_enrichment_with_address_persists_despite_zero_ext_id():
|
|
||||||
"""#1820: ext_id=0 НО есть адрес → НЕ пропускаем (developmentPage с адресом, без avitoId)."""
|
|
||||||
db = MagicMock()
|
|
||||||
db.execute.return_value = MagicMock()
|
|
||||||
|
|
||||||
enrichment = HouseCatalogEnrichment(
|
|
||||||
house_url="https://www.avito.ru/catalog/novostroyki/dom-C/",
|
|
||||||
house=HouseInfo(ext_id=0, short_address="ул. Ленина, 1"),
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"app.services.scrapers.avito_houses.match_or_create_house",
|
|
||||||
return_value=(77, 0.9, "fingerprint"),
|
|
||||||
) as mock_match:
|
|
||||||
result = save_house_catalog_enrichment(db, enrichment)
|
|
||||||
|
|
||||||
# Адрес есть → персистим через fingerprint tier
|
|
||||||
mock_match.assert_called_once()
|
|
||||||
assert result["house_id"] == 77
|
|
||||||
|
|
|
||||||
|
|
@ -111,21 +111,6 @@ def test_unified_runs_negative_offset_422(client: TestClient) -> None:
|
||||||
assert r.status_code == 422
|
assert r.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
def test_unified_runs_invalid_status_422(client: TestClient) -> None:
|
|
||||||
"""status с непредусмотренным значением → 422 (Literal-валидация)."""
|
|
||||||
r = client.get("/api/v1/admin/scrape/runs", params={"status": "unknown_status"})
|
|
||||||
assert r.status_code == 422
|
|
||||||
|
|
||||||
|
|
||||||
def test_unified_runs_valid_statuses_200(client: TestClient) -> None:
|
|
||||||
"""Все допустимые значения status проходят валидацию (ожидаем 200, не 422)."""
|
|
||||||
valid_statuses = ["done", "running", "banned", "zombie", "failed", "cancelled"]
|
|
||||||
for s in valid_statuses:
|
|
||||||
with patch("app.services.scrape_runs.list_all", return_value=(0, [])):
|
|
||||||
r = client.get("/api/v1/admin/scrape/runs", params={"status": s})
|
|
||||||
assert r.status_code == 200, f"status={s!r} должен возвращать 200, получили {r.status_code}"
|
|
||||||
|
|
||||||
|
|
||||||
# ── API 2: GET /scraper/health ───────────────────────────────────────────────
|
# ── API 2: GET /scraper/health ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue