fix(app): полная перезапись — все шаги пайплайна + ZIP-обработка
- ZIP: elements объединяются из всех файлов - LLM: extractor.extract(text) - diff: differ.diff(prev, rows) - spec_rows + spec_history - Без дубликатов, без replace_string_in_file
This commit is contained in:
+38
-56
@@ -1,15 +1,8 @@
|
|||||||
"""
|
"""app.py — Точка входа и сборка слоёв. Бизнес-логики НОЛЬ."""
|
||||||
app.py — Точка входа и сборка слоёв.
|
|
||||||
|
|
||||||
Бизнес-логики НОЛЬ. Только:
|
|
||||||
1. Инициализация схемы БД (schema.ensure_schema)
|
|
||||||
2. Регистрация Blueprint-ов (test, api — будут добавляться)
|
|
||||||
|
|
||||||
Правило: если в app.py появляется if/for или бизнес-слово — это лишнее.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from flask import Flask, render_template, request
|
from flask import Flask, render_template, request
|
||||||
|
import json
|
||||||
|
|
||||||
import schema
|
import schema
|
||||||
import db
|
import db
|
||||||
@@ -26,44 +19,27 @@ load_dotenv()
|
|||||||
|
|
||||||
|
|
||||||
class ContractsApp:
|
class ContractsApp:
|
||||||
"""
|
|
||||||
Главный класс приложения.
|
|
||||||
Собирает все слои вместе, не содержит логики.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.app = Flask(__name__)
|
self.app = Flask(__name__)
|
||||||
|
|
||||||
# 1. Создать таблицы при старте (если их ещё нет)
|
|
||||||
schema.ensure_schema()
|
schema.ensure_schema()
|
||||||
|
|
||||||
# 2. Собрать маршруты
|
|
||||||
self._add_routes()
|
self._add_routes()
|
||||||
|
|
||||||
def _add_routes(self):
|
def _add_routes(self):
|
||||||
"""Регистрация всех Blueprint-ов и системных маршрутов."""
|
|
||||||
|
|
||||||
# -- Системные маршруты --
|
|
||||||
self.app.add_url_rule("/", "index", self._upload_page, methods=["GET", "POST"])
|
self.app.add_url_rule("/", "index", self._upload_page, methods=["GET", "POST"])
|
||||||
self.app.add_url_rule("/health", "health", self._health)
|
self.app.add_url_rule("/health", "health", self._health)
|
||||||
|
|
||||||
# -- Blueprint-ы (каждый слой — отдельный Blueprint) --
|
|
||||||
self.app.register_blueprint(test_bp)
|
self.app.register_blueprint(test_bp)
|
||||||
self.app.register_blueprint(upload_bp)
|
self.app.register_blueprint(upload_bp)
|
||||||
self.app.register_blueprint(api_bp)
|
self.app.register_blueprint(api_bp)
|
||||||
|
|
||||||
def _health(self):
|
def _health(self):
|
||||||
"""Health check для платформы."""
|
|
||||||
return "OK", 200, {"Content-Type": "text/plain"}
|
return "OK", 200, {"Content-Type": "text/plain"}
|
||||||
|
|
||||||
def _upload_page(self):
|
def _upload_page(self):
|
||||||
"""Страница загрузки: GET — форма, POST — обработка."""
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
return self._process_upload()
|
return self._process_upload()
|
||||||
return render_template("upload.html")
|
return render_template("upload.html")
|
||||||
|
|
||||||
def _process_upload(self):
|
def _process_upload(self):
|
||||||
"""Обработка загруженных файлов."""
|
|
||||||
files = request.files.getlist("files")
|
files = request.files.getlist("files")
|
||||||
if not files or not any(f.filename for f in files):
|
if not files or not any(f.filename for f in files):
|
||||||
return render_template("upload.html", error="Нет файлов")
|
return render_template("upload.html", error="Нет файлов")
|
||||||
@@ -72,7 +48,6 @@ class ContractsApp:
|
|||||||
conn, err = db.connect()
|
conn, err = db.connect()
|
||||||
if err:
|
if err:
|
||||||
return render_template("upload.html", error=f"БД: {err}")
|
return render_template("upload.html", error=f"БД: {err}")
|
||||||
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id",
|
"INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id",
|
||||||
@@ -89,14 +64,13 @@ class ContractsApp:
|
|||||||
for f in files:
|
for f in files:
|
||||||
if not f.filename:
|
if not f.filename:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
filename = f.filename
|
filename = f.filename
|
||||||
mime = mimeutil.guess_mime(filename) or f.content_type or "application/octet-stream"
|
mime = mimeutil.guess_mime(filename) or f.content_type or "application/octet-stream"
|
||||||
file_bytes = f.read()
|
file_bytes = f.read()
|
||||||
|
|
||||||
# Сохранить документ
|
# Сохранить документ
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES (%s, %s, %s, 'uploaded')",
|
"INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES (%s,%s,%s,'uploaded')",
|
||||||
(filename, mime, file_bytes),
|
(filename, mime, file_bytes),
|
||||||
)
|
)
|
||||||
doc, _ = db.query_one(
|
doc, _ = db.query_one(
|
||||||
@@ -104,23 +78,39 @@ class ContractsApp:
|
|||||||
(filename,),
|
(filename,),
|
||||||
)
|
)
|
||||||
doc_id = doc["id"] if doc else None
|
doc_id = doc["id"] if doc else None
|
||||||
|
|
||||||
if not doc_id:
|
if not doc_id:
|
||||||
results.append({"name": filename, "ok": False, "error": "doc save failed"})
|
results.append({"name": filename, "ok": False, "error": "doc save failed"})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Парсинг
|
# Парсинг
|
||||||
pr = parser_mod.parse(file_bytes, mime)
|
pr = parser_mod.parse(file_bytes, mime)
|
||||||
if pr.get("errors"):
|
|
||||||
results.append({"name": filename, "ok": False, "error": "; ".join(pr["errors"])})
|
# ZIP: объединяем элементы из всех файлов
|
||||||
|
if mime == "application/zip" and "files" in pr:
|
||||||
|
all_el = []
|
||||||
|
zip_errs = []
|
||||||
|
for zf in pr["files"]:
|
||||||
|
if zf.get("errors"):
|
||||||
|
zip_errs.extend(zf["errors"])
|
||||||
|
all_el.extend(zf.get("elements", []))
|
||||||
|
if zip_errs and not all_el:
|
||||||
|
results.append({"name": filename, "ok": False, "error": "; ".join(zip_errs)})
|
||||||
|
continue
|
||||||
|
elements = all_el
|
||||||
|
else:
|
||||||
|
elements = pr.get("elements", [])
|
||||||
|
|
||||||
|
if not elements:
|
||||||
|
errs = pr.get("errors", [])
|
||||||
|
results.append({"name": filename, "ok": False, "error": "; ".join(errs) if errs else "нет таблиц"})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
text = textify.to_text(pr.get("elements", []))
|
text = textify.to_text(elements)
|
||||||
db.execute("UPDATE documents SET parsed_text=%s, status='parsed' WHERE id=%s", (text, str(doc_id)))
|
db.execute("UPDATE documents SET parsed_text=%s, status='parsed' WHERE id=%s", (text, str(doc_id)))
|
||||||
|
|
||||||
# Допник
|
# Допник
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT INTO supplements (contract_id, document_id, type) VALUES (%s, %s, 'initial')",
|
"INSERT INTO supplements (contract_id, document_id, type) VALUES (%s,%s,'initial')",
|
||||||
(str(contract_id), str(doc_id)),
|
(str(contract_id), str(doc_id)),
|
||||||
)
|
)
|
||||||
supp, _ = db.query_one(
|
supp, _ = db.query_one(
|
||||||
@@ -128,7 +118,6 @@ class ContractsApp:
|
|||||||
(str(doc_id),),
|
(str(doc_id),),
|
||||||
)
|
)
|
||||||
supp_id = supp["id"] if supp else None
|
supp_id = supp["id"] if supp else None
|
||||||
|
|
||||||
if not supp_id:
|
if not supp_id:
|
||||||
results.append({"name": filename, "ok": False, "error": "supplement save failed"})
|
results.append({"name": filename, "ok": False, "error": "supplement save failed"})
|
||||||
continue
|
continue
|
||||||
@@ -142,59 +131,52 @@ class ContractsApp:
|
|||||||
rows = ext.get("rows", [])
|
rows = ext.get("rows", [])
|
||||||
total_rows += len(rows)
|
total_rows += len(rows)
|
||||||
|
|
||||||
# Сохранить строки
|
# Сохранить spec_rows
|
||||||
for row in rows:
|
for row in rows:
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT INTO spec_rows (supplement_id, row_num, name, price, qty, sum, date_start) "
|
"INSERT INTO spec_rows (supplement_id,row_num,name,price,qty,sum,date_start) VALUES (%s,%s,%s,%s,%s,%s,%s)",
|
||||||
"VALUES (%s,%s,%s,%s,%s,%s,%s)",
|
|
||||||
(str(supp_id), row.get("row_num"), row.get("name"),
|
(str(supp_id), row.get("row_num"), row.get("name"),
|
||||||
row.get("price"), row.get("qty"), row.get("sum"), row.get("date_start")),
|
row.get("price"), row.get("qty"), row.get("sum"), row.get("date_start")),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Diff с предыдущим
|
# Diff с предыдущим
|
||||||
prev, _ = db.query(
|
prev_res, _ = db.query(
|
||||||
"SELECT sr.row_num, sr.name, sr.price, sr.qty, sr.sum, sr.date_start "
|
"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 "
|
"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",
|
"WHERE s.contract_id=%s AND s.id!=%s ORDER BY sr.row_num",
|
||||||
(str(contract_id), str(supp_id)),
|
(str(contract_id), str(supp_id)),
|
||||||
)
|
)
|
||||||
prev_rows = [dict(zip(prev["columns"], r)) for r in prev["rows"]] if prev else []
|
prev_rows = [dict(zip(prev_res["columns"], r)) for r in prev_res["rows"]] if prev_res else []
|
||||||
diff_r = differ.diff(prev_rows, rows)
|
diff_r = differ.diff(prev_rows, rows)
|
||||||
|
|
||||||
for ch in diff_r.get("changes", []):
|
for ch in diff_r.get("changes", []):
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT INTO spec_history (contract_id, supplement_id, row_num, change_type, old_values, new_values) "
|
"INSERT INTO spec_history (contract_id,supplement_id,row_num,change_type,old_values,new_values) "
|
||||||
"VALUES (%s,%s,%s,%s,%s,%s)",
|
"VALUES (%s,%s,%s,%s,%s,%s)",
|
||||||
(str(contract_id), str(supp_id), ch["row_num"], ch["change_type"],
|
(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,
|
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),
|
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),))
|
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", {})})
|
results.append({"name": filename, "ok": True, "rows": len(rows), "summary": diff_r.get("summary", {})})
|
||||||
|
|
||||||
# Получить финальные строки для отображения
|
# Все строки для отображения
|
||||||
all_rows = []
|
all_rows = []
|
||||||
rows_result, _ = db.query(
|
rows_res, _ = db.query(
|
||||||
"SELECT sr.row_num, sr.name, sr.price, sr.qty, sr.sum, sr.date_start "
|
"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 "
|
"FROM spec_rows sr JOIN supplements s ON sr.supplement_id=s.id "
|
||||||
"WHERE s.contract_id=%s ORDER BY sr.row_num",
|
"WHERE s.contract_id=%s ORDER BY sr.row_num",
|
||||||
(str(contract_id),),
|
(str(contract_id),),
|
||||||
)
|
)
|
||||||
if rows_result:
|
if rows_res:
|
||||||
all_rows = [dict(zip(rows_result["columns"], r)) for r in rows_result["rows"]]
|
all_rows = [dict(zip(rows_res["columns"], r)) for r in rows_res["rows"]]
|
||||||
|
|
||||||
return render_template("upload.html", results=results, all_rows=all_rows, total_rows=total_rows)
|
return render_template("upload.html", results=results, all_rows=all_rows, total_rows=total_rows)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Запуск Flask (только для разработки, в production — gunicorn)."""
|
|
||||||
self.app.run(host="0.0.0.0", port=5000)
|
self.app.run(host="0.0.0.0", port=5000)
|
||||||
|
|
||||||
|
|
||||||
# ── Точка входа ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app_instance = ContractsApp()
|
ContractsApp().run()
|
||||||
app_instance.run()
|
|
||||||
app_instance = ContractsApp()
|
|
||||||
app_instance.run()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user