Files
contracts-app/site/app.py
T

199 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""app.py — Точка входа и сборка слоёв."""
from dotenv import load_dotenv
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
import json
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__)
schema.ensure_schema()
self._add_routes()
def _add_routes(self):
self.app.add_url_rule("/", "index", self._index, methods=["GET", "POST"])
self.app.add_url_rule("/parse/<cid>", "parse", self._parse, methods=["GET"])
self.app.add_url_rule("/health", "health", self._health)
self.app.register_blueprint(test_bp)
self.app.register_blueprint(upload_bp)
self.app.register_blueprint(api_bp)
def _health(self):
return "OK", 200, {"Content-Type": "text/plain"}
# ── Шаг 1: загрузка файлов ──────────────────────────────────
def _index(self):
if request.method == "POST":
return self._upload_files()
cid = request.args.get("id")
if cid:
return self._show_contract(cid)
return render_template("upload.html")
def _upload_files(self):
"""Только сохранить файлы, без LLM."""
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()
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 doc_id:
db.execute(
"INSERT INTO supplements (contract_id, document_id, type) VALUES (%s,%s,'initial')",
(str(contract_id), str(doc_id)),
)
return jsonify({"contract_id": str(contract_id)})
def _show_contract(self, cid):
"""Показать договор с кнопкой «Обработать»."""
contract, _ = db.query_one("SELECT id,number,created_at FROM contracts WHERE id=%s", (cid,))
if not contract:
return render_template("upload.html", error="Договор не найден")
supps, _ = db.query(
"SELECT s.id, s.created_at, d.filename, d.status, d.parsed_text IS NOT NULL as has_text "
"FROM supplements s JOIN documents d ON s.document_id=d.id "
"WHERE s.contract_id=%s ORDER BY s.created_at",
(cid,),
)
supp_list = [dict(zip(supps["columns"], r)) for r in supps["rows"]] if supps else []
# Есть ли уже результаты?
rows_res, _ = 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",
(cid,),
)
all_rows = [dict(zip(rows_res["columns"], r)) for r in rows_res["rows"]] if rows_res else []
# Есть необработанные допники?
unprocessed = [s for s in supp_list if s["status"] in ("uploaded", "parsed")]
return render_template("upload.html",
contract=contract, supplements=supp_list,
all_rows=all_rows, unprocessed=unprocessed)
# ── Шаг 2: Парсинг (SSE) ────────────────────────────────────
def _parse(self, cid):
"""SSE-поток: парсинг файлов договора (без LLM)."""
import time as time_mod
def generate():
start_time = time_mod.time()
total_bytes = 0
files_processed = 0
supps, _ = db.query(
"SELECT s.id, d.id as doc_id, d.filename, d.mime_type, "
"LENGTH(d.original_bytes) as file_size "
"FROM supplements s JOIN documents d ON s.document_id=d.id "
"WHERE s.contract_id=%s",
(cid,),
)
if not supps:
yield f"data: {json.dumps({'type': 'error', 'message': 'Нет файлов'})}\n\n"
return
supp_rows = [dict(zip(supps["columns"], r)) for r in supps["rows"]]
for s in supp_rows:
# Пропустить уже распарсенные
existing, _ = db.query(
"SELECT 1 FROM documents WHERE id=%s AND status='parsed'", (s["doc_id"],)
)
if existing and existing["rows"]:
continue
file_start = time_mod.time()
file_bytes = s.get("file_size", 0) or 0
total_bytes += file_bytes
yield f"data: {json.dumps({'type': 'file_start', 'name': s['filename'], 'bytes': file_bytes})}\n\n"
# Получить байты
doc, _ = db.query_one("SELECT original_bytes FROM documents WHERE id=%s", (s["doc_id"],))
if not doc or not doc.get("original_bytes"):
yield f"data: {json.dumps({'type': 'file_error', 'name': s['filename'], 'error': 'Нет данных'})}\n\n"
continue
raw = doc["original_bytes"]
file_data = bytes(raw) if isinstance(raw, memoryview) else raw
# Парсинг
pr = parser_mod.parse(file_data, s["mime_type"])
elements = []
if s["mime_type"] == "application/zip" and "files" in pr:
for zf in pr["files"]:
elements.extend(zf.get("elements", []))
else:
elements = pr.get("elements", [])
text = textify.to_text(elements) if elements else ""
db.execute(
"UPDATE documents SET parsed_text=%s, status='parsed' WHERE id=%s",
(text, str(s["doc_id"])),
)
elapsed = round(time_mod.time() - file_start, 1)
files_processed += 1
yield f"data: {json.dumps({'type': 'file_done', 'name': s['filename'], 'time_s': elapsed, 'elements': len(elements)})}\n\n"
total_time = round(time_mod.time() - start_time, 1)
yield f"data: {json.dumps({'type': 'summary', 'total_time_s': total_time, 'total_bytes': total_bytes, 'files_processed': files_processed})}\n\n"
return Response(generate(), mimetype="text/event-stream")
def run(self):
self.app.run(host="0.0.0.0", port=5000)
if __name__ == "__main__":
ContractsApp().run()