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:
2026-06-14 06:11:23 +04:00
parent ba2e8552e7
commit 8db35e3b9d
2 changed files with 118 additions and 0 deletions
+116
View File
@@ -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")