Initial commit: Real Estate Registry app
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, File, Form, Request, UploadFile
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from config import UPLOAD_DIR
|
||||
from database.connection import get_db
|
||||
from services.extractor import extract_document, get_available_providers, get_default_provider
|
||||
from services.pdf_handler import pdf_to_images
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
ALLOWED_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
ALLOWED_PDF_EXTS = {".pdf"}
|
||||
ALLOWED_EXTENSIONS = ALLOWED_IMAGE_EXTS | ALLOWED_PDF_EXTS
|
||||
|
||||
|
||||
def _save_image(file_bytes: bytes, original_name: str) -> str:
|
||||
"""Save image to uploads/{date}/{uuid}_{name} and return relative path."""
|
||||
today = date.today().isoformat()
|
||||
dest_dir = Path(UPLOAD_DIR) / today
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
suffix = Path(original_name).suffix.lower() or ".jpg"
|
||||
filename = f"{uuid.uuid4().hex}{suffix}"
|
||||
dest = dest_dir / filename
|
||||
dest.write_bytes(file_bytes)
|
||||
return str(Path(today) / filename)
|
||||
|
||||
|
||||
async def _extract_and_save(doc_id: int, image_path: str, provider: str = ""):
|
||||
"""Background task: call extractor, parse result, update DB."""
|
||||
try:
|
||||
data = await extract_document(image_path, provider=provider)
|
||||
raw_json = json.dumps(data, ensure_ascii=False)
|
||||
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"""UPDATE documents SET status='extracted',
|
||||
raw_extraction_json=?,
|
||||
request_number=?,
|
||||
request_date=?,
|
||||
applicant_name_raw=?,
|
||||
request_purpose=?,
|
||||
data_valid_until=?,
|
||||
registry_office=?,
|
||||
owns_properties=?,
|
||||
declared_property_count=?,
|
||||
page_info=?,
|
||||
search_scope=?,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
WHERE id=?""",
|
||||
(
|
||||
raw_json,
|
||||
data.get("request_number"),
|
||||
data.get("request_date"),
|
||||
data.get("applicant_name_raw"),
|
||||
data.get("request_purpose"),
|
||||
data.get("data_valid_until"),
|
||||
data.get("registry_office"),
|
||||
data.get("owns_properties"),
|
||||
data.get("declared_property_count"),
|
||||
data.get("page_info"),
|
||||
data.get("search_scope"),
|
||||
doc_id,
|
||||
),
|
||||
)
|
||||
|
||||
for i, prop in enumerate(data.get("properties", [])):
|
||||
conn.execute(
|
||||
"""INSERT INTO properties
|
||||
(document_id, row_order, party_name, property_number,
|
||||
section, block, real_estate_district, qaza, num_shares, ownership_type)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
doc_id, i,
|
||||
prop.get("party_name"),
|
||||
prop.get("property_number"),
|
||||
prop.get("section"),
|
||||
prop.get("block"),
|
||||
prop.get("real_estate_district"),
|
||||
prop.get("qaza"),
|
||||
prop.get("num_shares"),
|
||||
prop.get("ownership_type"),
|
||||
),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"""UPDATE documents SET status='error', extraction_error=?,
|
||||
updated_at=CURRENT_TIMESTAMP WHERE id=?""",
|
||||
(str(e), doc_id),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def upload_page(request: Request):
|
||||
with get_db() as conn:
|
||||
stats = conn.execute(
|
||||
"""SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN status='confirmed' THEN 1 ELSE 0 END) AS confirmed,
|
||||
SUM(CASE WHEN status='extracted' THEN 1 ELSE 0 END) AS pending_review,
|
||||
SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors
|
||||
FROM documents"""
|
||||
).fetchone()
|
||||
return templates.TemplateResponse(
|
||||
request, "index.html", {
|
||||
"stats": dict(stats) if stats else {},
|
||||
"providers": get_available_providers(),
|
||||
"default_provider": get_default_provider(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_files(
|
||||
request: Request,
|
||||
files: list[UploadFile] = File(...),
|
||||
provider: str = Form(""),
|
||||
):
|
||||
if not provider:
|
||||
provider = get_default_provider()
|
||||
|
||||
doc_ids = []
|
||||
|
||||
for upload in files:
|
||||
suffix = Path(upload.filename).suffix.lower()
|
||||
if suffix not in ALLOWED_EXTENSIONS:
|
||||
continue
|
||||
|
||||
file_bytes = await upload.read()
|
||||
|
||||
if suffix in ALLOWED_PDF_EXTS:
|
||||
# PDF: split into per-page images
|
||||
pages = pdf_to_images(file_bytes, upload.filename)
|
||||
for page_info in pages:
|
||||
with get_db() as conn:
|
||||
cursor = conn.execute(
|
||||
"""INSERT INTO documents
|
||||
(image_path, status, provider, pdf_group_id, page_number)
|
||||
VALUES (?, 'pending', ?, ?, ?)""",
|
||||
(
|
||||
page_info["image_path"],
|
||||
provider,
|
||||
page_info["pdf_group_id"],
|
||||
page_info["page_number"],
|
||||
),
|
||||
)
|
||||
doc_ids.append((cursor.lastrowid, page_info["image_path"]))
|
||||
else:
|
||||
# Image file
|
||||
rel_path = _save_image(file_bytes, upload.filename)
|
||||
with get_db() as conn:
|
||||
cursor = conn.execute(
|
||||
"INSERT INTO documents (image_path, status, provider) VALUES (?, 'pending', ?)",
|
||||
(rel_path, provider),
|
||||
)
|
||||
doc_ids.append((cursor.lastrowid, rel_path))
|
||||
|
||||
# Fire background extractions
|
||||
for doc_id, rel_path in doc_ids:
|
||||
asyncio.create_task(_extract_and_save(doc_id, rel_path, provider))
|
||||
|
||||
if len(doc_ids) == 1:
|
||||
return RedirectResponse(f"/review/{doc_ids[0][0]}?wait=1", status_code=303)
|
||||
return RedirectResponse("/documents?uploaded=1", status_code=303)
|
||||
Reference in New Issue
Block a user