File size: 3,097 Bytes
41c9648
55ca1bd
aa93eef
 
 
 
e5d586b
aa93eef
 
 
 
 
41c9648
 
 
 
 
 
 
 
 
 
 
 
 
 
55ca1bd
 
 
 
 
 
aa93eef
 
e5d586b
 
 
 
 
 
 
 
aa93eef
 
 
 
 
 
 
 
 
 
 
 
55ca1bd
 
aa93eef
55ca1bd
aa93eef
 
55ca1bd
 
aa93eef
 
41c9648
 
aa93eef
 
 
 
 
41c9648
aa93eef
41c9648
aa93eef
 
55ca1bd
 
 
aa93eef
 
 
 
 
 
 
 
 
 
41c9648
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import csv
import logging
import os
import tempfile

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse

from model import extract_contact
from vcf import generate_vcf, VCF_DIR

CSV_FIELDS = ["name", "company", "designation", "phone", "email", "website", "address"]
CSV_PATH = os.path.join(VCF_DIR, "contacts.csv")


def append_csv(data):
    # ponytail: append-only log of every extraction; write header once on first row
    new_file = not os.path.exists(CSV_PATH)
    with open(CSV_PATH, "a", newline="") as f:
        w = csv.DictWriter(f, fieldnames=CSV_FIELDS, extrasaction="ignore")
        if new_file:
            w.writeheader()
        w.writerow({k: data.get(k, "") for k in CSV_FIELDS})
    return os.path.basename(CSV_PATH)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s: %(message)s",
)
log = logging.getLogger("cardextractor")

app = FastAPI(title="Business Card Extractor", version="1.0")

# ponytail: allow-all CORS; tighten to your frontend origin if this ever leaves a demo
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/")
def home():
    return {"message": "Business Card API", "endpoint": "/extract"}


@app.post("/extract")
async def extract(file: UploadFile = File(...)):
    image_path = None  # ponytail: defined up front so finally never UnboundLocalErrors
    try:
        suffix = os.path.splitext(file.filename)[1]
        with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp:
            body = await file.read()
            temp.write(body)
            image_path = temp.name
        log.info("received %s (%d bytes) -> %s", file.filename, len(body), image_path)

        data = extract_contact(image_path)
        log.info("extracted: %s", data)

        vcf_path = generate_vcf(data)
        filename = os.path.basename(vcf_path)
        csv_name = append_csv(data)
        log.info("wrote vcf: %s, appended csv: %s", filename, csv_name)

        return {
            "success": True,
            "contact": data,
            "vcf": filename,
            "csv": csv_name,
            "download": f"/download/{filename}",
            "csv_download": f"/download/{csv_name}",
        }
    except Exception as e:
        # ponytail: exc_info dumps the full traceback to the container logs
        log.exception("extract failed for %s", file.filename)
        raise HTTPException(500, f"{type(e).__name__}: {e}")
    finally:
        if image_path and os.path.exists(image_path):
            os.remove(image_path)


@app.get("/download/{filename}")
def download(filename: str):
    # ponytail: basename strips any ../ path traversal from the URL
    path = os.path.join(VCF_DIR, os.path.basename(filename))
    if not os.path.exists(path):
        raise HTTPException(404, "file not found")
    media = "text/csv" if path.endswith(".csv") else "text/vcard"
    return FileResponse(path, filename=filename, media_type=media)