Нормализовать переводы строк в 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:
Rustam Khalimov
2026-09-11 19:01:42 +03:00
parent 39c9bdc1b7
commit 713d554dc2
751 changed files with 94507 additions and 94486 deletions
@@ -1,164 +1,164 @@
"""Тест: автономный ML-сервис + очередь входящих (этап1 -> ML -> ИИ) + outbox.
Запускает локальный ML-сервис (mlservice/server.py) на порту 8121, учит его
напрямую по HTTP и проверяет весь контур основного приложения:
* обучение всегда идёт через outbox (даже при выключенном ML в пайплайне);
* /api/ml/predict, /api/ml/candidates, /api/ml/apply;
* очередь: stop-фраза -> удаляется; ML уверен -> карточка сразу на доску.
"""
import os
import subprocess
import sys
import tempfile
import time
import httpx
TMP = tempfile.mkdtemp(prefix="leadradar_pl_")
os.environ["LEADRADAR_DATA"] = os.path.join(TMP, "app")
os.environ["LEADRADAR_ML_URL"] = "http://127.0.0.1:8121"
os.environ["LEADRADAR_DEMO"] = "1"
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ML_DIR = os.path.join(os.path.dirname(BACKEND_DIR), "mlservice")
# ── локальный ML-сервис ───────────────────────────────────────────────────
ml_env = dict(os.environ)
ml_env["ML_DATA"] = os.path.join(TMP, "ml.duckdb")
ml_server = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "server:app", "--port", "8121", "--log-level", "warning"],
cwd=ML_DIR,
env=ml_env,
)
try:
for _ in range(50):
try:
if httpx.get("http://127.0.0.1:8121/health", timeout=1).status_code == 200:
break
except Exception:
time.sleep(0.3)
else:
raise SystemExit("ml service did not start")
sys.path.insert(0, BACKEND_DIR)
from fastapi.testclient import TestClient # noqa: E402
from app.main import app # noqa: E402
from app.services import pipeline as pl # noqa: E402
ML = "http://127.0.0.1:8121"
SPAM_TEXTS = [
"Заработок на крипте 300 процентов в месяц гарантировано подпишись на канал",
"Инвестируй в наш фонд и получай пассивный доход каждый день без риска",
"Трейдинг бот приносит 1000 долларов в день забери свою прибыль сейчас",
"Бесплатный курс по заработку на бирже забери по ссылке внизу поста",
"Приглашаю в закрытый чат заработка пассивно без вложений начни сегодня",
"Схема быстрого заработка на арбитраже крипты без риска все проверено",
"Купи сигналы на форекс и зарабатывай миллионы пока все спят от нас",
"Пирамида дохода открыла набор новых участников успей вложиться",
]
PY_TEXTS = [
"Нужен Python разработчик для телеграм бота парсера маркетплейсов удаленно",
"Ищем middle python бекенд разработчика на fastapi для стартапа удаленка",
"Задача для python джуна написать скрипт парсинга авито с антидетектом",
"Python разработчик на django проект CRM интеграция с телеграм ботом",
"Нужен python программист для автоматизации отчетов и бота в телеграм",
"Срочно python разработчик aiogram телеграм бот для интернет магазина",
"Python backend для API на fastapi микросервисы postgres kafka",
"Разработчик python на скрапинг каталогов маркетплейсов выгрузка в excel",
"Ищем python специалиста для интеграции с мессенджерами и crm",
"Python разработчик на парсер и бота оплата достойная сразу в лс",
]
FRONT_TEXTS = [
"Frontend разработчик vuejs для корпоративного портала удаленная работа",
"Нужен верстальщик реакт для интернет магазина срочно до конца недели",
"Ищем frontend специалиста vue3 typescript компоненты дизайн система",
"Задача для фронтендера сверстать адаптивный лендинг на nuxtjs",
"Frontend разработчик react nextjs для панели администратора стартапа",
"Верстка писем и лендингов html css для маркетинговых рассылок заказ",
"Нужен vue разработчик доработка фронта для телеграм мини апп",
"Frontend инженер angular для банковского приложения гибрид офис",
]
# учим ML-сервис напрямую (как если бы фоновый воркер отправил outbox)
# — перенесено внутрь with: метки = id колонок, которые создаём через API
with TestClient(app) as client:
assert client.post("/api/auth/login", json={"login": "admin", "password": "admin"}).status_code == 200
client.patch("/api/settings", json={"aiProvider": "ollama", "mlEnabled": True})
# колонок по умолчанию нет — создаём Python и Frontend
py = client.post("/api/boards", json={"name": "Python"}).json()["id"]
fr = client.post("/api/boards", json={"name": "Frontend"}).json()["id"]
for t in PY_TEXTS + FRONT_TEXTS:
httpx.post(ML + "/learn", json={"label": py if t in PY_TEXTS else fr, "text": t})
for t in SPAM_TEXTS:
httpx.post(ML + "/learn", json={"label": "spam", "text": t})
def wait_until(pred, seconds=8):
"""Фоновый воркер разбирает очередь сам — опрашиваем до наступления условия."""
deadline = time.time() + seconds
last = None
while time.time() < deadline:
last = client.post("/api/admin/tick").json()
if pred():
return last
time.sleep(0.5)
raise AssertionError("условие не наступило: %s" % pred())
def leads():
return client.get("/api/leads").json()["items"]
# статус/предсказание через основное приложение
st = client.get("/api/ml/status").json()
assert st["reachable"] and st["service"]["ready"], st
p = client.post("/api/ml/predict", json={"text": "Python backend на fastapi парсер телеграм удаленно"}).json()
assert p["take"] and p["label"] == py, p
print("ml status/predict ok")
# стоп-фраза -> удаляется из очереди, ничего не оседает
pl.enqueue("d1", "Канал А", "", "#333", 1, "Ищу работу на неделю, вот моё резюме и портфолио для отклика", 1_788_000_000_000)
assert pl.queue_len() == 1
wait_until(lambda: pl.queue_len() == 0)
assert len(leads()) == 0
print("stop-phrase drop ok")
# ML уверен -> карточка сразу на доску (без ИИ), спам -> удаление
pl.enqueue("d1", "Канал А", "", "#333", 3, "Python backend на fastapi для стартапа, бот в телеграм, удалённо", 1_788_000_200_000)
pl.enqueue("d1", "Канал А", "", "#333", 4, "Заработок на крипте инвестируй в наш фонд пассивный доход каждый день", 1_788_000_300_000)
wait_until(lambda: pl.queue_len() == 0 and any(l["col"] == py for l in leads()))
card = [l for l in leads() if l["col"] == py and l["sourceMsgId"] == 3]
assert len(card) == 1, leads()
print("ml fast-path ok")
# ручная разметка apply: spam -> карточка в корзину + обучение (outbox)
r = client.post("/api/ml/apply", json={"dialogId": "d1", "msgId": 3, "action": "spam"}).json()
assert r["learned"] is True and r["moved"] == "trash", r
assert client.get("/api/ml/status").json()["stats"]["outbox"] >= 1
# flush -> обучение уехало в ML-сервис
f = client.post("/api/ml/flush").json()
assert f["outbox"] == 0 and f["flushed"] >= 1, f
print("apply + outbox ok")
# обучение идёт всегда, даже если ML выключен в пайплайне
pl.enqueue("d1", "Канал А", "", "#333", 7, "Frontend vue разработка интерфейса компоненты верстка реакт удаленно", 1_788_000_500_000)
wait_until(lambda: any(l["col"] == fr for l in leads()))
lead = [l for l in leads() if l["col"] == fr][0]
boards = client.get("/api/boards").json()
client.patch("/api/settings", json={"mlEnabled": False})
client.post(f"/api/leads/{lead['id']}/move", json={"to": py})
assert client.get("/api/ml/status").json()["stats"]["outbox"] == 1
print("learn-always (ml off) ok")
# кандидаты канала: последние сообщения (после демо-лида их нет в TG -> fallback)
cand = client.post("/api/ml/candidates", json={"dialogId": "d1", "limit": 5}).json()
assert isinstance(cand.get("items"), list)
print("candidates ok")
print("PIPELINE TEST OK")
finally:
ml_server.terminate()
try:
ml_server.wait(timeout=10)
except Exception:
ml_server.kill()
"""Тест: автономный ML-сервис + очередь входящих (этап1 -> ML -> ИИ) + outbox.
Запускает локальный ML-сервис (mlservice/server.py) на порту 8121, учит его
напрямую по HTTP и проверяет весь контур основного приложения:
* обучение всегда идёт через outbox (даже при выключенном ML в пайплайне);
* /api/ml/predict, /api/ml/candidates, /api/ml/apply;
* очередь: stop-фраза -> удаляется; ML уверен -> карточка сразу на доску.
"""
import os
import subprocess
import sys
import tempfile
import time
import httpx
TMP = tempfile.mkdtemp(prefix="leadradar_pl_")
os.environ["LEADRADAR_DATA"] = os.path.join(TMP, "app")
os.environ["LEADRADAR_ML_URL"] = "http://127.0.0.1:8121"
os.environ["LEADRADAR_DEMO"] = "1"
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ML_DIR = os.path.join(os.path.dirname(BACKEND_DIR), "mlservice")
# ── локальный ML-сервис ───────────────────────────────────────────────────
ml_env = dict(os.environ)
ml_env["ML_DATA"] = os.path.join(TMP, "ml.duckdb")
ml_server = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "server:app", "--port", "8121", "--log-level", "warning"],
cwd=ML_DIR,
env=ml_env,
)
try:
for _ in range(50):
try:
if httpx.get("http://127.0.0.1:8121/health", timeout=1).status_code == 200:
break
except Exception:
time.sleep(0.3)
else:
raise SystemExit("ml service did not start")
sys.path.insert(0, BACKEND_DIR)
from fastapi.testclient import TestClient # noqa: E402
from app.main import app # noqa: E402
from app.services import pipeline as pl # noqa: E402
ML = "http://127.0.0.1:8121"
SPAM_TEXTS = [
"Заработок на крипте 300 процентов в месяц гарантировано подпишись на канал",
"Инвестируй в наш фонд и получай пассивный доход каждый день без риска",
"Трейдинг бот приносит 1000 долларов в день забери свою прибыль сейчас",
"Бесплатный курс по заработку на бирже забери по ссылке внизу поста",
"Приглашаю в закрытый чат заработка пассивно без вложений начни сегодня",
"Схема быстрого заработка на арбитраже крипты без риска все проверено",
"Купи сигналы на форекс и зарабатывай миллионы пока все спят от нас",
"Пирамида дохода открыла набор новых участников успей вложиться",
]
PY_TEXTS = [
"Нужен Python разработчик для телеграм бота парсера маркетплейсов удаленно",
"Ищем middle python бекенд разработчика на fastapi для стартапа удаленка",
"Задача для python джуна написать скрипт парсинга авито с антидетектом",
"Python разработчик на django проект CRM интеграция с телеграм ботом",
"Нужен python программист для автоматизации отчетов и бота в телеграм",
"Срочно python разработчик aiogram телеграм бот для интернет магазина",
"Python backend для API на fastapi микросервисы postgres kafka",
"Разработчик python на скрапинг каталогов маркетплейсов выгрузка в excel",
"Ищем python специалиста для интеграции с мессенджерами и crm",
"Python разработчик на парсер и бота оплата достойная сразу в лс",
]
FRONT_TEXTS = [
"Frontend разработчик vuejs для корпоративного портала удаленная работа",
"Нужен верстальщик реакт для интернет магазина срочно до конца недели",
"Ищем frontend специалиста vue3 typescript компоненты дизайн система",
"Задача для фронтендера сверстать адаптивный лендинг на nuxtjs",
"Frontend разработчик react nextjs для панели администратора стартапа",
"Верстка писем и лендингов html css для маркетинговых рассылок заказ",
"Нужен vue разработчик доработка фронта для телеграм мини апп",
"Frontend инженер angular для банковского приложения гибрид офис",
]
# учим ML-сервис напрямую (как если бы фоновый воркер отправил outbox)
# — перенесено внутрь with: метки = id колонок, которые создаём через API
with TestClient(app) as client:
assert client.post("/api/auth/login", json={"login": "admin", "password": "admin"}).status_code == 200
client.patch("/api/settings", json={"aiProvider": "ollama", "mlEnabled": True})
# колонок по умолчанию нет — создаём Python и Frontend
py = client.post("/api/boards", json={"name": "Python"}).json()["id"]
fr = client.post("/api/boards", json={"name": "Frontend"}).json()["id"]
for t in PY_TEXTS + FRONT_TEXTS:
httpx.post(ML + "/learn", json={"label": py if t in PY_TEXTS else fr, "text": t})
for t in SPAM_TEXTS:
httpx.post(ML + "/learn", json={"label": "spam", "text": t})
def wait_until(pred, seconds=8):
"""Фоновый воркер разбирает очередь сам — опрашиваем до наступления условия."""
deadline = time.time() + seconds
last = None
while time.time() < deadline:
last = client.post("/api/admin/tick").json()
if pred():
return last
time.sleep(0.5)
raise AssertionError("условие не наступило: %s" % pred())
def leads():
return client.get("/api/leads").json()["items"]
# статус/предсказание через основное приложение
st = client.get("/api/ml/status").json()
assert st["reachable"] and st["service"]["ready"], st
p = client.post("/api/ml/predict", json={"text": "Python backend на fastapi парсер телеграм удаленно"}).json()
assert p["take"] and p["label"] == py, p
print("ml status/predict ok")
# стоп-фраза -> удаляется из очереди, ничего не оседает
pl.enqueue("d1", "Канал А", "", "#333", 1, "Ищу работу на неделю, вот моё резюме и портфолио для отклика", 1_788_000_000_000)
assert pl.queue_len() == 1
wait_until(lambda: pl.queue_len() == 0)
assert len(leads()) == 0
print("stop-phrase drop ok")
# ML уверен -> карточка сразу на доску (без ИИ), спам -> удаление
pl.enqueue("d1", "Канал А", "", "#333", 3, "Python backend на fastapi для стартапа, бот в телеграм, удалённо", 1_788_000_200_000)
pl.enqueue("d1", "Канал А", "", "#333", 4, "Заработок на крипте инвестируй в наш фонд пассивный доход каждый день", 1_788_000_300_000)
wait_until(lambda: pl.queue_len() == 0 and any(l["col"] == py for l in leads()))
card = [l for l in leads() if l["col"] == py and l["sourceMsgId"] == 3]
assert len(card) == 1, leads()
print("ml fast-path ok")
# ручная разметка apply: spam -> карточка в корзину + обучение (outbox)
r = client.post("/api/ml/apply", json={"dialogId": "d1", "msgId": 3, "action": "spam"}).json()
assert r["learned"] is True and r["moved"] == "trash", r
assert client.get("/api/ml/status").json()["stats"]["outbox"] >= 1
# flush -> обучение уехало в ML-сервис
f = client.post("/api/ml/flush").json()
assert f["outbox"] == 0 and f["flushed"] >= 1, f
print("apply + outbox ok")
# обучение идёт всегда, даже если ML выключен в пайплайне
pl.enqueue("d1", "Канал А", "", "#333", 7, "Frontend vue разработка интерфейса компоненты верстка реакт удаленно", 1_788_000_500_000)
wait_until(lambda: any(l["col"] == fr for l in leads()))
lead = [l for l in leads() if l["col"] == fr][0]
boards = client.get("/api/boards").json()
client.patch("/api/settings", json={"mlEnabled": False})
client.post(f"/api/leads/{lead['id']}/move", json={"to": py})
assert client.get("/api/ml/status").json()["stats"]["outbox"] == 1
print("learn-always (ml off) ok")
# кандидаты канала: последние сообщения (после демо-лида их нет в TG -> fallback)
cand = client.post("/api/ml/candidates", json={"dialogId": "d1", "limit": 5}).json()
assert isinstance(cand.get("items"), list)
print("candidates ok")
print("PIPELINE TEST OK")
finally:
ml_server.terminate()
try:
ml_server.wait(timeout=10)
except Exception:
ml_server.kill()