Нормализовать переводы строк в LF
Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF, CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения), .editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize. Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
This commit is contained in:
@@ -1,216 +1,216 @@
|
||||
"""Точка входа FastAPI-приложения LeadRadar.
|
||||
|
||||
Запуск: uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
или: python -m app.main
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import config
|
||||
from .auth import ensure_creds
|
||||
from .db import store
|
||||
from .routers import (
|
||||
auth_routes,
|
||||
dashboard_routes,
|
||||
discovery_routes,
|
||||
events_routes,
|
||||
ml_routes,
|
||||
processing_routes,
|
||||
projects_routes,
|
||||
settings_routes,
|
||||
tg_routes,
|
||||
)
|
||||
from .services import fts as fts_svc
|
||||
from .services import ml_client
|
||||
from .services import projects as proj_svc
|
||||
from .services import rates as rates_svc
|
||||
from .services.leads import notify_tick_stats, tick_storage
|
||||
from .services.telegram import register_main_loop, tg
|
||||
|
||||
logging.basicConfig(level=config.LOG_LEVEL, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("leadradar")
|
||||
|
||||
|
||||
async def _storage_loop() -> None:
|
||||
"""Каждые 30 секунд: правила хранения (архив/очистка), напоминания, сердцебиение Telegram."""
|
||||
while True:
|
||||
try:
|
||||
stats = tick_storage()
|
||||
await notify_tick_stats(stats)
|
||||
await proj_svc.check_reminders()
|
||||
await tg.heartbeat()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("storage loop error")
|
||||
await asyncio.sleep(30)
|
||||
|
||||
|
||||
async def _fts_loop() -> None:
|
||||
"""Полнотекстовые индексы пересобираем раз в сутки (FTS — снимок)."""
|
||||
while True:
|
||||
await asyncio.sleep(24 * 3600)
|
||||
try:
|
||||
fts_svc.rebuild()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("fts rebuild error")
|
||||
|
||||
|
||||
async def _rates_loop() -> None:
|
||||
"""Курсы ЦБ РФ: проверяем раз в 30 минут, тянем не чаще 1 раза в 6 часов."""
|
||||
while True:
|
||||
try:
|
||||
if rates_svc.should_fetch():
|
||||
ok = await rates_svc.refresh_rates()
|
||||
if not ok:
|
||||
log.warning("rates fetch failed (повтор через 30 минут)")
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("rates loop error")
|
||||
await asyncio.sleep(30 * 60)
|
||||
|
||||
|
||||
async def _pipeline_loop() -> None:
|
||||
"""Фоновый воркер очереди входящих: разбирает сообщения каждые 2 секунды."""
|
||||
from .services.pipeline import pump_once
|
||||
|
||||
while True:
|
||||
try:
|
||||
await pump_once()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("pipeline worker error")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
|
||||
async def _discovery_loop() -> None:
|
||||
"""Фоновый воркер Discovery: поиск → оценка → авто-вступление (раз в 5 c)."""
|
||||
from .services.discovery_worker import tick
|
||||
|
||||
while True:
|
||||
try:
|
||||
await tick()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("discovery worker error")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
async def _ml_sync_loop() -> None:
|
||||
"""Отправка событий обучения в ML-сервис + кэш его статуса (каждые 10 c)."""
|
||||
while True:
|
||||
try:
|
||||
await ml_client.flush_outbox()
|
||||
await ml_client.refresh_status()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("ml sync error")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
async def _suggest_loop() -> None:
|
||||
"""Раз в 3 минуты пробуем предложить колонки по «Неразобранному» (кулдаун 20 мин)."""
|
||||
from .services.suggest import suggest_from_inbox
|
||||
|
||||
while True:
|
||||
try:
|
||||
await suggest_from_inbox(force=False)
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("suggest loop error")
|
||||
await asyncio.sleep(180)
|
||||
|
||||
|
||||
async def _tg_sweep_loop() -> None:
|
||||
"""Раз в 30 с: страховочная догонялка непрочитанного по включённым каналам
|
||||
(событие могло потеряться при рестарте/разрыве соединения)."""
|
||||
from .services.telegram import tg as tg_svc
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(30)
|
||||
try:
|
||||
await tg_svc.realtime_sweep()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("tg realtime sweep error")
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
config.ensure_dirs()
|
||||
store.init()
|
||||
ensure_creds()
|
||||
register_main_loop(asyncio.get_running_loop())
|
||||
tasks = [
|
||||
asyncio.create_task(_storage_loop()),
|
||||
asyncio.create_task(_rates_loop()),
|
||||
asyncio.create_task(_fts_loop()),
|
||||
asyncio.create_task(_pipeline_loop()),
|
||||
asyncio.create_task(_discovery_loop()),
|
||||
asyncio.create_task(_ml_sync_loop()),
|
||||
asyncio.create_task(_suggest_loop()),
|
||||
asyncio.create_task(_tg_sweep_loop()),
|
||||
asyncio.create_task(tg.auto_resume()),
|
||||
]
|
||||
# полнотекстовый поиск: пробуем установить расширение и собрать индекс
|
||||
if fts_svc.install_extension():
|
||||
fts_svc.rebuild()
|
||||
# первичное обновление курсов из ЦБ (если источник cbr)
|
||||
if store.get_setting("rateSource") == "cbr":
|
||||
try:
|
||||
await rates_svc.refresh_rates()
|
||||
except Exception: # noqa: BLE001
|
||||
log.warning("initial rates fetch failed; продолжаем с мок-курсами")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
await tg.disconnect()
|
||||
store.close()
|
||||
|
||||
|
||||
app = FastAPI(title="LeadRadar", version="1.2.0", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # локальный dev-режим; в проде замените на свой origin
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# API
|
||||
for mod in (auth_routes, tg_routes, dashboard_routes, projects_routes, settings_routes, events_routes, discovery_routes, ml_routes, processing_routes):
|
||||
app.include_router(mod.router)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health() -> dict:
|
||||
return {"ok": True, "service": "leadradar"}
|
||||
|
||||
|
||||
# Статика фронтенда (собранный dist). Если папки нет — API живёт отдельно.
|
||||
_DIST = config.FRONTEND_DIST
|
||||
if _DIST.is_dir():
|
||||
assets = _DIST / "assets"
|
||||
if assets.is_dir():
|
||||
app.mount("/assets", StaticFiles(directory=str(assets)), name="assets")
|
||||
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
async def spa(full_path: str):
|
||||
candidate = (_DIST / full_path).resolve()
|
||||
if full_path and candidate.is_file() and candidate.is_relative_to(_DIST):
|
||||
return FileResponse(str(candidate))
|
||||
index = _DIST / "index.html"
|
||||
if index.exists():
|
||||
return FileResponse(str(index))
|
||||
return JSONResponse({"detail": "фронтенд не собран — используйте npm run dev"}, status_code=200)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("app.main:app", host=config.HOST, port=config.PORT, reload=False)
|
||||
"""Точка входа FastAPI-приложения LeadRadar.
|
||||
|
||||
Запуск: uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
или: python -m app.main
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import config
|
||||
from .auth import ensure_creds
|
||||
from .db import store
|
||||
from .routers import (
|
||||
auth_routes,
|
||||
dashboard_routes,
|
||||
discovery_routes,
|
||||
events_routes,
|
||||
ml_routes,
|
||||
processing_routes,
|
||||
projects_routes,
|
||||
settings_routes,
|
||||
tg_routes,
|
||||
)
|
||||
from .services import fts as fts_svc
|
||||
from .services import ml_client
|
||||
from .services import projects as proj_svc
|
||||
from .services import rates as rates_svc
|
||||
from .services.leads import notify_tick_stats, tick_storage
|
||||
from .services.telegram import register_main_loop, tg
|
||||
|
||||
logging.basicConfig(level=config.LOG_LEVEL, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("leadradar")
|
||||
|
||||
|
||||
async def _storage_loop() -> None:
|
||||
"""Каждые 30 секунд: правила хранения (архив/очистка), напоминания, сердцебиение Telegram."""
|
||||
while True:
|
||||
try:
|
||||
stats = tick_storage()
|
||||
await notify_tick_stats(stats)
|
||||
await proj_svc.check_reminders()
|
||||
await tg.heartbeat()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("storage loop error")
|
||||
await asyncio.sleep(30)
|
||||
|
||||
|
||||
async def _fts_loop() -> None:
|
||||
"""Полнотекстовые индексы пересобираем раз в сутки (FTS — снимок)."""
|
||||
while True:
|
||||
await asyncio.sleep(24 * 3600)
|
||||
try:
|
||||
fts_svc.rebuild()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("fts rebuild error")
|
||||
|
||||
|
||||
async def _rates_loop() -> None:
|
||||
"""Курсы ЦБ РФ: проверяем раз в 30 минут, тянем не чаще 1 раза в 6 часов."""
|
||||
while True:
|
||||
try:
|
||||
if rates_svc.should_fetch():
|
||||
ok = await rates_svc.refresh_rates()
|
||||
if not ok:
|
||||
log.warning("rates fetch failed (повтор через 30 минут)")
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("rates loop error")
|
||||
await asyncio.sleep(30 * 60)
|
||||
|
||||
|
||||
async def _pipeline_loop() -> None:
|
||||
"""Фоновый воркер очереди входящих: разбирает сообщения каждые 2 секунды."""
|
||||
from .services.pipeline import pump_once
|
||||
|
||||
while True:
|
||||
try:
|
||||
await pump_once()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("pipeline worker error")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
|
||||
async def _discovery_loop() -> None:
|
||||
"""Фоновый воркер Discovery: поиск → оценка → авто-вступление (раз в 5 c)."""
|
||||
from .services.discovery_worker import tick
|
||||
|
||||
while True:
|
||||
try:
|
||||
await tick()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("discovery worker error")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
async def _ml_sync_loop() -> None:
|
||||
"""Отправка событий обучения в ML-сервис + кэш его статуса (каждые 10 c)."""
|
||||
while True:
|
||||
try:
|
||||
await ml_client.flush_outbox()
|
||||
await ml_client.refresh_status()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("ml sync error")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
async def _suggest_loop() -> None:
|
||||
"""Раз в 3 минуты пробуем предложить колонки по «Неразобранному» (кулдаун 20 мин)."""
|
||||
from .services.suggest import suggest_from_inbox
|
||||
|
||||
while True:
|
||||
try:
|
||||
await suggest_from_inbox(force=False)
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("suggest loop error")
|
||||
await asyncio.sleep(180)
|
||||
|
||||
|
||||
async def _tg_sweep_loop() -> None:
|
||||
"""Раз в 30 с: страховочная догонялка непрочитанного по включённым каналам
|
||||
(событие могло потеряться при рестарте/разрыве соединения)."""
|
||||
from .services.telegram import tg as tg_svc
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(30)
|
||||
try:
|
||||
await tg_svc.realtime_sweep()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("tg realtime sweep error")
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
config.ensure_dirs()
|
||||
store.init()
|
||||
ensure_creds()
|
||||
register_main_loop(asyncio.get_running_loop())
|
||||
tasks = [
|
||||
asyncio.create_task(_storage_loop()),
|
||||
asyncio.create_task(_rates_loop()),
|
||||
asyncio.create_task(_fts_loop()),
|
||||
asyncio.create_task(_pipeline_loop()),
|
||||
asyncio.create_task(_discovery_loop()),
|
||||
asyncio.create_task(_ml_sync_loop()),
|
||||
asyncio.create_task(_suggest_loop()),
|
||||
asyncio.create_task(_tg_sweep_loop()),
|
||||
asyncio.create_task(tg.auto_resume()),
|
||||
]
|
||||
# полнотекстовый поиск: пробуем установить расширение и собрать индекс
|
||||
if fts_svc.install_extension():
|
||||
fts_svc.rebuild()
|
||||
# первичное обновление курсов из ЦБ (если источник cbr)
|
||||
if store.get_setting("rateSource") == "cbr":
|
||||
try:
|
||||
await rates_svc.refresh_rates()
|
||||
except Exception: # noqa: BLE001
|
||||
log.warning("initial rates fetch failed; продолжаем с мок-курсами")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
await tg.disconnect()
|
||||
store.close()
|
||||
|
||||
|
||||
app = FastAPI(title="LeadRadar", version="1.2.0", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # локальный dev-режим; в проде замените на свой origin
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# API
|
||||
for mod in (auth_routes, tg_routes, dashboard_routes, projects_routes, settings_routes, events_routes, discovery_routes, ml_routes, processing_routes):
|
||||
app.include_router(mod.router)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health() -> dict:
|
||||
return {"ok": True, "service": "leadradar"}
|
||||
|
||||
|
||||
# Статика фронтенда (собранный dist). Если папки нет — API живёт отдельно.
|
||||
_DIST = config.FRONTEND_DIST
|
||||
if _DIST.is_dir():
|
||||
assets = _DIST / "assets"
|
||||
if assets.is_dir():
|
||||
app.mount("/assets", StaticFiles(directory=str(assets)), name="assets")
|
||||
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
async def spa(full_path: str):
|
||||
candidate = (_DIST / full_path).resolve()
|
||||
if full_path and candidate.is_file() and candidate.is_relative_to(_DIST):
|
||||
return FileResponse(str(candidate))
|
||||
index = _DIST / "index.html"
|
||||
if index.exists():
|
||||
return FileResponse(str(index))
|
||||
return JSONResponse({"detail": "фронтенд не собран — используйте npm run dev"}, status_code=200)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("app.main:app", host=config.HOST, port=config.PORT, reload=False)
|
||||
|
||||
Reference in New Issue
Block a user