feat(upload): слой загрузки документов + регистрация в app.py
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
This commit is contained in:
@@ -15,6 +15,7 @@ from flask import Flask, render_template
|
|||||||
import db
|
import db
|
||||||
import schema
|
import schema
|
||||||
from test_routes import test_bp
|
from test_routes import test_bp
|
||||||
|
from upload import upload_bp
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ class ContractsApp:
|
|||||||
|
|
||||||
# -- Blueprint-ы (каждый слой — отдельный Blueprint) --
|
# -- Blueprint-ы (каждый слой — отдельный Blueprint) --
|
||||||
self.app.register_blueprint(test_bp)
|
self.app.register_blueprint(test_bp)
|
||||||
|
self.app.register_blueprint(upload_bp)
|
||||||
# self.app.register_blueprint(api_bp) # ← будет добавлен позже
|
# self.app.register_blueprint(api_bp) # ← будет добавлен позже
|
||||||
|
|
||||||
def _index(self):
|
def _index(self):
|
||||||
|
|||||||
+116
@@ -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")
|
||||||
Reference in New Issue
Block a user