From 8db35e3b9db8c342ed7f46ffe137e5e7643ca7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 14 Jun 2026 06:11:23 +0400 Subject: [PATCH] =?UTF-8?q?feat(upload):=20=D1=81=D0=BB=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20+=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B2=20app.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /upload (multipart/form-data: file): 1. Сохраняет original_bytes в documents (status='uploaded') 2. parser.parse() → elements 3. textify.to_text() → parsed_text 4. UPDATE documents SET parsed_text, status='parsed' 5. Возвращает {document_id, filename, status, elements_count} При ошибке парсинга: status='error', error_message, 422 --- site/app.py | 2 + site/upload.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 site/upload.py diff --git a/site/app.py b/site/app.py index 2b2e339..f62f4ee 100644 --- a/site/app.py +++ b/site/app.py @@ -15,6 +15,7 @@ from flask import Flask, render_template import db import schema from test_routes import test_bp +from upload import upload_bp load_dotenv() @@ -43,6 +44,7 @@ class ContractsApp: # -- Blueprint-ы (каждый слой — отдельный Blueprint) -- self.app.register_blueprint(test_bp) + self.app.register_blueprint(upload_bp) # self.app.register_blueprint(api_bp) # ← будет добавлен позже def _index(self): diff --git a/site/upload.py b/site/upload.py new file mode 100644 index 0000000..318ad1b --- /dev/null +++ b/site/upload.py @@ -0,0 +1,116 @@ +""" +upload.py — Слой загрузки документов. + +Blueprint /upload: принимает файлы, сохраняет в БД, запускает парсинг. + +Поток: + POST /upload (multipart/form-data: file) + → сохраняем original_bytes в documents (status='uploaded') + → parser.parse() → elements + → textify.to_text() → parsed_text + → UPDATE documents SET parsed_text, status='parsed' + → возвращаем document_id + +Не зависит от LLM-слоёв. Только от parser.py, textify.py, db.py. +""" + +from flask import Blueprint, request, jsonify +import db +import parser +import textify + +upload_bp = Blueprint("upload", __name__) + + +@upload_bp.route("/upload", methods=["POST"]) +def upload(): + """ + Загрузить файл договора/допника. + + Ожидает multipart/form-data с полем 'file'. + Возвращает {document_id, filename, status}. + """ + # 1. Проверить что файл передан + if "file" not in request.files: + return jsonify({"error": "no file in request"}), 400 + + file = request.files["file"] + if not file.filename: + return jsonify({"error": "empty filename"}), 400 + + # 2. Определить MIME-тип + filename = file.filename + mime_type = file.content_type or _guess_mime(filename) + file_bytes = file.read() + + if not file_bytes: + return jsonify({"error": "empty file"}), 400 + + # 3. Сохранить исходный файл в БД + rowcount, err = db.execute( + "INSERT INTO documents (filename, mime_type, original_bytes, status) " + "VALUES (%s, %s, %s, 'uploaded')", + (filename, mime_type, file_bytes), + ) + if err: + return jsonify({"error": f"db insert: {err}"}), 500 + + # 4. Получить ID созданного документа + doc, err = db.query_one( + "SELECT id FROM documents " + "WHERE filename = %s AND status = 'uploaded' " + "ORDER BY created_at DESC LIMIT 1", + (filename,), + ) + if err or not doc: + return jsonify({"error": f"document not found after insert: {err}"}), 500 + + document_id = doc["id"] + + # 5. Парсинг: bytes → elements → текст + parse_result = parser.parse(file_bytes, mime_type) + + if parse_result.get("errors"): + # Парсинг частично не удался — сохраняем что смогли, помечаем ошибку + db.execute( + "UPDATE documents SET status = 'error', error_message = %s WHERE id = %s", + ("; ".join(parse_result["errors"]), str(document_id)), + ) + return jsonify({ + "document_id": str(document_id), + "filename": filename, + "status": "error", + "errors": parse_result["errors"], + }), 422 + + # 6. JSON elements → линейный текст для LLM + elements = parse_result.get("elements", []) + parsed_text = textify.to_text(elements) + + # 7. Обновить документ: текст + статус + db.execute( + "UPDATE documents SET parsed_text = %s, status = 'parsed' WHERE id = %s", + (parsed_text, str(document_id)), + ) + + return jsonify({ + "document_id": str(document_id), + "filename": filename, + "status": "parsed", + "elements_count": len(elements), + }) + + +def _guess_mime(filename: str) -> str: + """ + Угадать MIME-тип по расширению файла. + Используется когда браузер не передал content_type. + """ + ext = filename.lower().rsplit(".", 1)[-1] if "." in filename else "" + mime_map = { + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "doc": "application/msword", + "pdf": "application/pdf", + "zip": "application/zip", + } + return mime_map.get(ext, "application/octet-stream")