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:
+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