Убран JS fetch. Форма отправляется напрямую на сервер. Сервер обрабатывает файлы и возвращает страницу с результатами. Нет Failed to fetch.
201 lines
7.8 KiB
Python
201 lines
7.8 KiB
Python
"""
|
||
app.py — Точка входа и сборка слоёв.
|
||
|
||
Бизнес-логики НОЛЬ. Только:
|
||
1. Инициализация схемы БД (schema.ensure_schema)
|
||
2. Регистрация Blueprint-ов (test, api — будут добавляться)
|
||
|
||
Правило: если в app.py появляется if/for или бизнес-слово — это лишнее.
|
||
"""
|
||
|
||
from dotenv import load_dotenv
|
||
from flask import Flask, render_template, request
|
||
|
||
import schema
|
||
import db
|
||
import parser as parser_mod
|
||
import textify
|
||
import extractor
|
||
import differ
|
||
import mimeutil
|
||
from test_routes import test_bp
|
||
from upload import upload_bp
|
||
from api import api_bp
|
||
|
||
load_dotenv()
|
||
|
||
|
||
class ContractsApp:
|
||
"""
|
||
Главный класс приложения.
|
||
Собирает все слои вместе, не содержит логики.
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.app = Flask(__name__)
|
||
|
||
# 1. Создать таблицы при старте (если их ещё нет)
|
||
schema.ensure_schema()
|
||
|
||
# 2. Собрать маршруты
|
||
self._add_routes()
|
||
|
||
def _add_routes(self):
|
||
"""Регистрация всех Blueprint-ов и системных маршрутов."""
|
||
|
||
# -- Системные маршруты --
|
||
self.app.add_url_rule("/", "index", self._upload_page)
|
||
self.app.add_url_rule("/health", "health", self._health)
|
||
|
||
# -- Blueprint-ы (каждый слой — отдельный Blueprint) --
|
||
self.app.register_blueprint(test_bp)
|
||
self.app.register_blueprint(upload_bp)
|
||
self.app.register_blueprint(api_bp)
|
||
|
||
def _health(self):
|
||
"""Health check для платформы."""
|
||
return "OK", 200, {"Content-Type": "text/plain"}
|
||
|
||
def _upload_page(self):
|
||
"""Страница загрузки: GET — форма, POST — обработка."""
|
||
if request.method == "POST":
|
||
return self._process_upload()
|
||
return render_template("upload.html")
|
||
|
||
def _process_upload(self):
|
||
"""Обработка загруженных файлов."""
|
||
files = request.files.getlist("files")
|
||
if not files or not any(f.filename for f in files):
|
||
return render_template("upload.html", error="Нет файлов")
|
||
|
||
# Создать договор
|
||
conn, err = db.connect()
|
||
if err:
|
||
return render_template("upload.html", error=f"БД: {err}")
|
||
|
||
cur = conn.cursor()
|
||
cur.execute(
|
||
"INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id",
|
||
("б/н " + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""),
|
||
)
|
||
contract_id = cur.fetchone()[0]
|
||
conn.commit()
|
||
cur.close()
|
||
conn.close()
|
||
|
||
results = []
|
||
total_rows = 0
|
||
|
||
for f in files:
|
||
if not f.filename:
|
||
continue
|
||
|
||
filename = f.filename
|
||
mime = mimeutil.guess_mime(filename) or f.content_type or "application/octet-stream"
|
||
file_bytes = f.read()
|
||
|
||
# Сохранить документ
|
||
db.execute(
|
||
"INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES (%s, %s, %s, 'uploaded')",
|
||
(filename, mime, file_bytes),
|
||
)
|
||
doc, _ = db.query_one(
|
||
"SELECT id FROM documents WHERE filename=%s AND status='uploaded' ORDER BY created_at DESC LIMIT 1",
|
||
(filename,),
|
||
)
|
||
doc_id = doc["id"] if doc else None
|
||
|
||
if not doc_id:
|
||
results.append({"name": filename, "ok": False, "error": "doc save failed"})
|
||
continue
|
||
|
||
# Парсинг
|
||
pr = parser_mod.parse(file_bytes, mime)
|
||
if pr.get("errors"):
|
||
results.append({"name": filename, "ok": False, "error": "; ".join(pr["errors"])})
|
||
continue
|
||
|
||
text = textify.to_text(pr.get("elements", []))
|
||
db.execute("UPDATE documents SET parsed_text=%s, status='parsed' WHERE id=%s", (text, str(doc_id)))
|
||
|
||
# Допник
|
||
db.execute(
|
||
"INSERT INTO supplements (contract_id, document_id, type) VALUES (%s, %s, 'initial')",
|
||
(str(contract_id), str(doc_id)),
|
||
)
|
||
supp, _ = db.query_one(
|
||
"SELECT id FROM supplements WHERE document_id=%s ORDER BY created_at DESC LIMIT 1",
|
||
(str(doc_id),),
|
||
)
|
||
supp_id = supp["id"] if supp else None
|
||
|
||
if not supp_id:
|
||
results.append({"name": filename, "ok": False, "error": "supplement save failed"})
|
||
continue
|
||
|
||
# LLM-извлечение
|
||
ext = extractor.extract(text)
|
||
if "error" in ext:
|
||
results.append({"name": filename, "ok": False, "error": ext["error"]})
|
||
continue
|
||
|
||
rows = ext.get("rows", [])
|
||
total_rows += len(rows)
|
||
|
||
# Сохранить строки
|
||
for row in rows:
|
||
db.execute(
|
||
"INSERT INTO spec_rows (supplement_id, row_num, name, price, qty, sum, date_start) "
|
||
"VALUES (%s,%s,%s,%s,%s,%s,%s)",
|
||
(str(supp_id), row.get("row_num"), row.get("name"),
|
||
row.get("price"), row.get("qty"), row.get("sum"), row.get("date_start")),
|
||
)
|
||
|
||
# Diff с предыдущим
|
||
prev, _ = db.query(
|
||
"SELECT sr.row_num, sr.name, sr.price, sr.qty, sr.sum, sr.date_start "
|
||
"FROM spec_rows sr JOIN supplements s ON sr.supplement_id=s.id "
|
||
"WHERE s.contract_id=%s AND s.id!=%s ORDER BY sr.row_num",
|
||
(str(contract_id), str(supp_id)),
|
||
)
|
||
prev_rows = [dict(zip(prev["columns"], r)) for r in prev["rows"]] if prev else []
|
||
diff_r = differ.diff(prev_rows, rows)
|
||
|
||
for ch in diff_r.get("changes", []):
|
||
db.execute(
|
||
"INSERT INTO spec_history (contract_id, supplement_id, row_num, change_type, old_values, new_values) "
|
||
"VALUES (%s,%s,%s,%s,%s,%s)",
|
||
(str(contract_id), str(supp_id), ch["row_num"], ch["change_type"],
|
||
__import__("json").dumps(ch.get("old_values"), ensure_ascii=False, default=str) if ch.get("old_values") else None,
|
||
__import__("json").dumps(ch.get("new_values"), ensure_ascii=False, default=str) if ch.get("new_values") else None),
|
||
)
|
||
|
||
db.execute("UPDATE documents SET status='extracted' WHERE id=%s", (str(doc_id),))
|
||
results.append({"name": filename, "ok": True, "rows": len(rows), "summary": diff_r.get("summary", {})})
|
||
|
||
# Получить финальные строки для отображения
|
||
all_rows = []
|
||
rows_result, _ = db.query(
|
||
"SELECT sr.row_num, sr.name, sr.price, sr.qty, sr.sum, sr.date_start "
|
||
"FROM spec_rows sr JOIN supplements s ON sr.supplement_id=s.id "
|
||
"WHERE s.contract_id=%s ORDER BY sr.row_num",
|
||
(str(contract_id),),
|
||
)
|
||
if rows_result:
|
||
all_rows = [dict(zip(rows_result["columns"], r)) for r in rows_result["rows"]]
|
||
|
||
return render_template("upload.html", results=results, all_rows=all_rows, total_rows=total_rows)
|
||
|
||
def run(self):
|
||
"""Запуск Flask (только для разработки, в production — gunicorn)."""
|
||
self.app.run(host="0.0.0.0", port=5000)
|
||
|
||
|
||
# ── Точка входа ─────────────────────────────────────────────────
|
||
|
||
if __name__ == "__main__":
|
||
app_instance = ContractsApp()
|
||
app_instance.run()
|
||
app_instance = ContractsApp()
|
||
app_instance.run()
|