fix: устранены все найденные аудитом баги и тихие падения
- SQL injection паттерн → параметризованные запросы во всех местах - except: pass/continue → logger.warning() везде, ничего не тонет молча - WAL mode + индекс domain_dedup_key в database.py - try/finally для conn в main.py, утечка соединения устранена - backoff 30с при 403/429 от Rusprofile/ЕГРЮЛ - ликвидированные компании → egrul_status="liquidated" - max_candidates в contacts_finder считает только реальных кандидатов - DB_PATH абсолютный (Path(__file__).parent), HH_PAUSE_BETWEEN_QUERIES в config - HH_SIGNAL_QUERIES дубль убран из launcher.py → импорт из config - path traversal защита в egrul_enricher debug_dump_html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ Streamlit перезапускает скрипт на каждое действ
|
|||||||
держать не имеет смысла.
|
держать не имеет смысла.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -15,6 +16,8 @@ from typing import Any
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _conn(db_path: Path | str) -> sqlite3.Connection:
|
def _conn(db_path: Path | str) -> sqlite3.Connection:
|
||||||
conn = sqlite3.connect(str(db_path))
|
conn = sqlite3.connect(str(db_path))
|
||||||
@@ -311,10 +314,17 @@ def add_lead_manual(db_path, data: dict) -> int:
|
|||||||
conn.rollback()
|
conn.rollback()
|
||||||
msg = str(e).lower()
|
msg = str(e).lower()
|
||||||
if "inn" in msg:
|
if "inn" in msg:
|
||||||
|
logger.warning(f"[db_layer] add_lead_manual: дубль по ИНН ({data.get('inn')}): {e}")
|
||||||
raise ValueError(f"Компания с таким ИНН уже есть в базе ({data.get('inn')}).") from e
|
raise ValueError(f"Компания с таким ИНН уже есть в базе ({data.get('inn')}).") from e
|
||||||
if "phone" in msg:
|
if "phone" in msg:
|
||||||
|
logger.warning(f"[db_layer] add_lead_manual: дубль по телефону: {e}")
|
||||||
raise ValueError(f"Компания с таким телефоном уже есть в базе.") from e
|
raise ValueError(f"Компания с таким телефоном уже есть в базе.") from e
|
||||||
|
logger.warning(f"[db_layer] add_lead_manual: IntegrityError: {e}")
|
||||||
raise ValueError(f"Не удалось добавить (дубль): {e}") from e
|
raise ValueError(f"Не удалось добавить (дубль): {e}") from e
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logger.warning(f"[db_layer] add_lead_manual: неожиданная ошибка: {e}")
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Все настройки в одном месте. Меняем здесь — отражается во всех парсерах.
|
Все настройки в одном месте. Меняем здесь — отражается во всех парсерах.
|
||||||
"""
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
# Города и их Yandex.Maps ID
|
# Города и их Yandex.Maps ID
|
||||||
@@ -113,9 +114,12 @@ MAX_CARDS_PER_CATEGORY = 100 # сколько карточек открыват
|
|||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
# Пути
|
# Пути
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
DB_PATH = "leads.db"
|
DB_PATH = Path(__file__).parent / "leads.db"
|
||||||
EXPORT_DIR = "exports"
|
EXPORT_DIR = "exports"
|
||||||
|
|
||||||
|
# Пауза между HH-запросами (секунды) — задаётся здесь и используется в run_hh
|
||||||
|
HH_PAUSE_BETWEEN_QUERIES = 3
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
# Скоринг лидов v5 — «решаемая нами боль» (шкала 0-10)
|
# Скоринг лидов v5 — «решаемая нами боль» (шкала 0-10)
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
+9
-3
@@ -4,6 +4,7 @@
|
|||||||
Сама занимается дедупликацией: ИНН > телефон > домен.
|
Сама занимается дедупликацией: ИНН > телефон > домен.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,6 +13,8 @@ from typing import Optional
|
|||||||
import config
|
import config
|
||||||
from normalization import phone_dedup_key, normalize_domain
|
from normalization import phone_dedup_key, normalize_domain
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
# Схема таблиц
|
# Схема таблиц
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
@@ -119,6 +122,7 @@ CREATE INDEX IF NOT EXISTS idx_leads_score ON leads(score DESC);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_leads_source ON leads(source);
|
CREATE INDEX IF NOT EXISTS idx_leads_source ON leads(source);
|
||||||
CREATE INDEX IF NOT EXISTS idx_leads_outreach ON leads(outreach_status);
|
CREATE INDEX IF NOT EXISTS idx_leads_outreach ON leads(outreach_status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_leads_has_website ON leads(has_website);
|
CREATE INDEX IF NOT EXISTS idx_leads_has_website ON leads(has_website);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_leads_domain ON leads(domain_dedup_key);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sources_log (
|
CREATE TABLE IF NOT EXISTS sources_log (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -208,6 +212,7 @@ def get_connection(db_path: str = "leads.db") -> sqlite3.Connection:
|
|||||||
"""Открыть соединение с включённым row_factory (sqlite3.Row для dict-доступа)."""
|
"""Открыть соединение с включённым row_factory (sqlite3.Row для dict-доступа)."""
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
@@ -748,8 +753,9 @@ def cleanup_bad_director_names(conn: sqlite3.Connection) -> int:
|
|||||||
"Производство", "Услуги", "Работ",
|
"Производство", "Услуги", "Работ",
|
||||||
]
|
]
|
||||||
# Совпадение если первое слово в director_name — должность
|
# Совпадение если первое слово в director_name — должность
|
||||||
where_parts = [f"director_name LIKE '{m}%'" for m in bad_markers]
|
where_parts = ["director_name LIKE ?"] * len(bad_markers)
|
||||||
where_clause = " OR ".join(where_parts)
|
where_clause = " OR ".join(where_parts)
|
||||||
|
params = [f"{m}%" for m in bad_markers]
|
||||||
sql = f"""
|
sql = f"""
|
||||||
UPDATE leads
|
UPDATE leads
|
||||||
SET director_name = NULL,
|
SET director_name = NULL,
|
||||||
@@ -757,7 +763,7 @@ def cleanup_bad_director_names(conn: sqlite3.Connection) -> int:
|
|||||||
egrul_status = NULL
|
egrul_status = NULL
|
||||||
WHERE {where_clause}
|
WHERE {where_clause}
|
||||||
"""
|
"""
|
||||||
cursor = conn.execute(sql)
|
cursor = conn.execute(sql, params)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount
|
return cursor.rowcount
|
||||||
|
|
||||||
@@ -878,7 +884,7 @@ def get_stats(conn: sqlite3.Connection) -> dict:
|
|||||||
).fetchall())
|
).fetchall())
|
||||||
threshold = config.HOT_LEAD_THRESHOLD
|
threshold = config.HOT_LEAD_THRESHOLD
|
||||||
hot = conn.execute(
|
hot = conn.execute(
|
||||||
f"SELECT COUNT(*) FROM leads WHERE score >= {int(threshold)}"
|
"SELECT COUNT(*) FROM leads WHERE score >= ?", (int(threshold),)
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
with_phone = conn.execute(
|
with_phone = conn.execute(
|
||||||
"SELECT COUNT(*) FROM leads WHERE phone_primary IS NOT NULL"
|
"SELECT COUNT(*) FROM leads WHERE phone_primary IS NOT NULL"
|
||||||
|
|||||||
@@ -303,7 +303,8 @@ def find_company_website(
|
|||||||
}
|
}
|
||||||
|
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
max_candidates = 6 # проверим не больше 6 разных сайтов
|
max_candidates = 6 # проверим не больше 6 реальных (не агрегаторных) кандидатов
|
||||||
|
real_candidates = 0
|
||||||
|
|
||||||
for query in queries:
|
for query in queries:
|
||||||
try:
|
try:
|
||||||
@@ -324,7 +325,8 @@ def find_company_website(
|
|||||||
seen.add(host)
|
seen.add(host)
|
||||||
if not _is_company_site(found_url):
|
if not _is_company_site(found_url):
|
||||||
continue
|
continue
|
||||||
if len(seen) > max_candidates:
|
real_candidates += 1
|
||||||
|
if real_candidates > max_candidates:
|
||||||
break
|
break
|
||||||
|
|
||||||
root = f"{urlparse(found_url).scheme}://{host}"
|
root = f"{urlparse(found_url).scheme}://{host}"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ def _api_key() -> Optional[str]:
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
key = os.environ.get("DADATA_API_KEY")
|
key = os.environ.get("DADATA_API_KEY")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
logger.warning("[dadata] python-dotenv не установлен, .env не загружен")
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
@@ -282,8 +282,12 @@ def enrich_via_dadata(
|
|||||||
best = d
|
best = d
|
||||||
break
|
break
|
||||||
if not best:
|
if not best:
|
||||||
# Все ликвидированы — берём первого, не записываем в БД
|
# Все ликвидированы — берём первого, логируем, выставляем статус
|
||||||
|
logger.warning(f"[dadata] Все совпадения ликвидированы для '{name}'")
|
||||||
best = suggestions[0].get("data") or {}
|
best = suggestions[0].get("data") or {}
|
||||||
|
parsed = _parse_party(best)
|
||||||
|
parsed["egrul_status"] = "liquidated"
|
||||||
|
return parsed
|
||||||
|
|
||||||
parsed = _parse_party(best)
|
parsed = _parse_party(best)
|
||||||
return parsed
|
return parsed
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
Стратегия запросов:
|
Стратегия запросов:
|
||||||
- Случайный User-Agent
|
- Случайный User-Agent
|
||||||
- timeout 10 сек
|
- timeout 10 сек
|
||||||
- При 403/429 — пропускаем (логируем как 'error')
|
- При 403/429 — пауза 30 сек + пропускаем (логируем как 'error')
|
||||||
- Пауза между лидами в run_egrul_enrichment, не здесь
|
- Пауза между лидами в run_egrul_enrichment, не здесь
|
||||||
|
|
||||||
Ограничения:
|
Ограничения:
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@@ -379,7 +380,8 @@ def enrich_egrul_by_inn(
|
|||||||
allow_redirects=True,
|
allow_redirects=True,
|
||||||
)
|
)
|
||||||
if resp.status_code in (403, 429):
|
if resp.status_code in (403, 429):
|
||||||
logger.warning(f" Rusprofile blocked us ({resp.status_code}) для ИНН {inn}")
|
logger.warning(f"[egrul] Rusprofile вернул {resp.status_code}, пауза 30 сек (ИНН {inn})")
|
||||||
|
time.sleep(30)
|
||||||
return result
|
return result
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
return result
|
return result
|
||||||
@@ -610,10 +612,11 @@ def enrich_egrul(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code in (403, 429):
|
if resp.status_code in (403, 429):
|
||||||
logger.warning(f" Rusprofile blocked us ({resp.status_code}) для '{name}'")
|
logger.warning(f"[egrul] Rusprofile вернул {resp.status_code}, пауза 30 сек ('{name}')")
|
||||||
|
time.sleep(30)
|
||||||
return result
|
return result
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
logger.debug(f" Rusprofile вернул {resp.status_code} для '{name}'")
|
logger.warning(f"[egrul] Rusprofile вернул {resp.status_code} для '{name}'")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Если редирект на /id/N или /ip/N — Rusprofile уверен в матче.
|
# Если редирект на /id/N или /ip/N — Rusprofile уверен в матче.
|
||||||
@@ -657,10 +660,18 @@ def enrich_egrul(
|
|||||||
|
|
||||||
# Debug: сохранить HTML для отладки regex'ов
|
# Debug: сохранить HTML для отладки regex'ов
|
||||||
if debug_dump_html:
|
if debug_dump_html:
|
||||||
|
from pathlib import Path
|
||||||
|
dump_path = Path(debug_dump_html).resolve()
|
||||||
try:
|
try:
|
||||||
with open(debug_dump_html, "w", encoding="utf-8") as f:
|
dump_path.relative_to(Path.cwd())
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"[egrul] debug_dump_html путь вне CWD, пропускаю: {debug_dump_html}")
|
||||||
|
debug_dump_html = None
|
||||||
|
if debug_dump_html:
|
||||||
|
try:
|
||||||
|
with open(dump_path, "w", encoding="utf-8") as f:
|
||||||
f.write(html)
|
f.write(html)
|
||||||
logger.info(f" [debug] HTML сохранён в {debug_dump_html}")
|
logger.info(f" [debug] HTML сохранён в {dump_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" [debug] Не удалось сохранить HTML: {e}")
|
logger.warning(f" [debug] Не удалось сохранить HTML: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ False positives возможны (например, упоминание "yclien
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -273,6 +274,7 @@ def analyze_website(url: str, timeout: float = 6.0) -> dict:
|
|||||||
timeout=(timeout, timeout), verify=False, stream=False,
|
timeout=(timeout, timeout), verify=False, stream=False,
|
||||||
)
|
)
|
||||||
if sub_resp.status_code != 200:
|
if sub_resp.status_code != 200:
|
||||||
|
time.sleep(0.5)
|
||||||
continue
|
continue
|
||||||
sub_html = sub_resp.text[:MAX_HTML_BYTES]
|
sub_html = sub_resp.text[:MAX_HTML_BYTES]
|
||||||
sub_inn, sub_ogrn, sub_kpp = _extract_inn_ogrn_kpp(sub_html)
|
sub_inn, sub_ogrn, sub_kpp = _extract_inn_ogrn_kpp(sub_html)
|
||||||
@@ -281,8 +283,11 @@ def analyze_website(url: str, timeout: float = 6.0) -> dict:
|
|||||||
ogrn_val = ogrn_val or sub_ogrn
|
ogrn_val = ogrn_val or sub_ogrn
|
||||||
kpp_val = kpp_val or sub_kpp
|
kpp_val = kpp_val or sub_kpp
|
||||||
logger.debug(f" ИНН найден на {path}: {inn_val}")
|
logger.debug(f" ИНН найден на {path}: {inn_val}")
|
||||||
|
time.sleep(0.5)
|
||||||
break
|
break
|
||||||
except Exception:
|
time.sleep(0.5)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[website_analyzer] подстраница {path} сайта {url}: {e}")
|
||||||
continue
|
continue
|
||||||
if inn_val:
|
if inn_val:
|
||||||
result["inn"] = inn_val
|
result["inn"] = inn_val
|
||||||
|
|||||||
+1
-18
@@ -7,6 +7,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import config # для синхронизации дефолтных категорий (фокус-ЦА) с CLI
|
import config # для синхронизации дефолтных категорий (фокус-ЦА) с CLI
|
||||||
|
from config import HH_SIGNAL_QUERIES
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import questionary
|
import questionary
|
||||||
@@ -224,24 +225,6 @@ MOSCOW_DISTRICTS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# ── HH signal-запросы (из config) ────────────────────────────────────
|
|
||||||
HH_SIGNAL_QUERIES = [
|
|
||||||
"оператор ПК",
|
|
||||||
"оператор колл-центра",
|
|
||||||
"оператор технической поддержки",
|
|
||||||
"менеджер чата",
|
|
||||||
"менеджер по продажам без CRM",
|
|
||||||
"помощник руководителя",
|
|
||||||
"ассистент руководителя",
|
|
||||||
"офис-менеджер",
|
|
||||||
"администратор записи",
|
|
||||||
"администратор салона красоты",
|
|
||||||
"администратор клиники",
|
|
||||||
"ресепшн",
|
|
||||||
"бухгалтер 1С",
|
|
||||||
"помощник бухгалтера",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def ask(fn, *args, **kwargs):
|
def ask(fn, *args, **kwargs):
|
||||||
result = fn(*args, **kwargs, style=STYLE).ask()
|
result = fn(*args, **kwargs, style=STYLE).ask()
|
||||||
|
|||||||
@@ -205,8 +205,7 @@ def run_hh(conn, queries: list[str], city: str, max_pages: int = None) -> list[i
|
|||||||
|
|
||||||
# Пауза между запросами (anti-rate-limit)
|
# Пауза между запросами (anti-rate-limit)
|
||||||
if idx < len(queries):
|
if idx < len(queries):
|
||||||
import time
|
time.sleep(config.HH_PAUSE_BETWEEN_QUERIES)
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
return run_ids
|
return run_ids
|
||||||
|
|
||||||
@@ -272,7 +271,7 @@ def run_enrichment(conn, limit: int | None = None) -> int:
|
|||||||
conn.execute(f"UPDATE leads SET {sets} WHERE id = ?", vals)
|
conn.execute(f"UPDATE leads SET {sets} WHERE id = ?", vals)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f" ⚠ update inn/ogrn: {e}")
|
logger.warning(f" ⚠ update inn/ogrn: {e}")
|
||||||
|
|
||||||
# Если нашли ИНН на сайте, сразу идём в Rusprofile by-inn за директором
|
# Если нашли ИНН на сайте, сразу идём в Rusprofile by-inn за директором
|
||||||
rusprofile_note = ""
|
rusprofile_note = ""
|
||||||
@@ -302,7 +301,7 @@ def run_enrichment(conn, limit: int | None = None) -> int:
|
|||||||
director = egrul_info.get("director_name") or "—"
|
director = egrul_info.get("director_name") or "—"
|
||||||
rusprofile_note = f" +ЕГРЮЛ:директор={director[:30]}"
|
rusprofile_note = f" +ЕГРЮЛ:директор={director[:30]}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f" ⚠ Rusprofile by-inn: {e}")
|
logger.warning(f" ⚠ Rusprofile by-inn: {e}")
|
||||||
|
|
||||||
# Пересчитать score (теперь с Tier 2 данными + ИНН с сайта)
|
# Пересчитать score (теперь с Tier 2 данными + ИНН с сайта)
|
||||||
full_lead = dict(conn.execute("SELECT * FROM leads WHERE id = ?", (lead_id,)).fetchone())
|
full_lead = dict(conn.execute("SELECT * FROM leads WHERE id = ?", (lead_id,)).fetchone())
|
||||||
@@ -888,7 +887,7 @@ def main():
|
|||||||
# Раньше флаг ставился всегда → `--full --source yandex` гонял HH-обогащение по
|
# Раньше флаг ставился всегда → `--full --source yandex` гонял HH-обогащение по
|
||||||
# ВСЕЙ базе HH-лидов без сайта (~час впустую). Теперь — только при source hh/all
|
# ВСЕЙ базе HH-лидов без сайта (~час впустую). Теперь — только при source hh/all
|
||||||
# (как и обещает README: «+ --hh-enrich-websites если source=hh»).
|
# (как и обещает README: «+ --hh-enrich-websites если source=hh»).
|
||||||
args.hh_enrich_websites = args.source in ("hh", "all")
|
args.hh_enrich_websites = args.hh_enrich_websites or args.source in ("hh", "all")
|
||||||
args.find_sites = True # ⬅ ищем сайты для лидов где website пуст (DDG fallback)
|
args.find_sites = True # ⬅ ищем сайты для лидов где website пуст (DDG fallback)
|
||||||
args.enrich = True # → website-analyzer достаёт email/phone со страниц
|
args.enrich = True # → website-analyzer достаёт email/phone со страниц
|
||||||
args.enrich_egrul = True
|
args.enrich_egrul = True
|
||||||
@@ -914,14 +913,13 @@ def main():
|
|||||||
# Инициализация БД
|
# Инициализация БД
|
||||||
init_db(config.DB_PATH)
|
init_db(config.DB_PATH)
|
||||||
conn = get_connection(config.DB_PATH)
|
conn = get_connection(config.DB_PATH)
|
||||||
|
try:
|
||||||
# Только статистика — выходим сразу
|
# Только статистика — выходим сразу
|
||||||
if args.stats:
|
if args.stats:
|
||||||
stats = get_stats(conn)
|
stats = get_stats(conn)
|
||||||
print("\n📊 СТАТИСТИКА:")
|
print("\n📊 СТАТИСТИКА:")
|
||||||
for k, v in stats.items():
|
for k, v in stats.items():
|
||||||
print(f" {k}: {v}")
|
print(f" {k}: {v}")
|
||||||
conn.close()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Cleanup кривых ФИО директоров (одноразовая операция)
|
# Cleanup кривых ФИО директоров (одноразовая операция)
|
||||||
@@ -960,7 +958,6 @@ def main():
|
|||||||
or args.export_run is not None
|
or args.export_run is not None
|
||||||
):
|
):
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
conn.close()
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Собираем run_id всех прогонов этой сессии — для auto-export CSV по прогонам
|
# Собираем run_id всех прогонов этой сессии — для auto-export CSV по прогонам
|
||||||
@@ -1062,7 +1059,7 @@ def main():
|
|||||||
# Шаг 7: пересоздать CSV конкретного прогона (--export-run N)
|
# Шаг 7: пересоздать CSV конкретного прогона (--export-run N)
|
||||||
if args.export_run is not None:
|
if args.export_run is not None:
|
||||||
export_run(config.DB_PATH, args.export_run)
|
export_run(config.DB_PATH, args.export_run)
|
||||||
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user