-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
230 lines (195 loc) · 6.32 KB
/
Copy pathapp.py
File metadata and controls
230 lines (195 loc) · 6.32 KB
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# backend/app.py
import os, re, uuid, json, httpx, asyncpg
from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Body
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
import csv
# load full catalog on startup
CATALOG: list[str] = []
with open("unique_fastener_catalog.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
CATALOG = [row["Description"] for row in reader]
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
EXTRACT_API_URL = os.getenv("EXTRACT_API_URL")
MATCH_API_URL = os.getenv("MATCH_API_URL")
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "uploads")
SAVE_DIR = os.getenv("SAVE_DIR", "saves")
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(SAVE_DIR, exist_ok=True)
app = FastAPI(title="Document Processing Backend")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# we'll keep a global pool
pool: asyncpg.Pool
@app.on_event("startup")
async def on_startup():
global pool
pool = await asyncpg.create_pool(DATABASE_URL)
@app.on_event("shutdown")
async def on_shutdown():
await pool.close()
def parse_number(raw_val):
if raw_val is None:
return 0
s = str(raw_val).replace(",", "")
m = re.search(r"[\d.]+", s)
return float(m.group(0)) if m else 0
def normalize_item(raw: dict) -> dict:
# <your existing normalization>
if raw.get("Qty") is not None:
qty = raw["Qty"]
elif raw.get("Quantity") is not None:
qty = raw["Quantity"]
else:
qty = raw.get("Amount") or 0
up = raw.get("Cost") or raw.get("Price") or raw.get("Unit Cost") or raw.get("Unit Price")
if raw.get("Total") is not None:
tot = raw["Total"]
elif (raw.get("Qty") or raw.get("Quantity")) and raw.get("Amount") is not None:
tot = raw["Amount"]
else:
tot = raw.get("Ext Cost") or 0
return {
"Quantity": parse_number(qty),
"Request Item": raw.get("Request Item") or raw.get("Item") or "",
"Unit Price": parse_number(up),
"Total Amount": parse_number(tot),
}
@app.post("/extract")
async def extract(file: UploadFile = File(...)):
# 1) read & save PDF locally
contents = await file.read()
path = os.path.join(UPLOAD_DIR, file.filename)
with open(path, "wb") as f:
f.write(contents)
# 2) forward to your extraction API
files = {"file": (file.filename, contents, file.content_type)}
async with httpx.AsyncClient() as client:
resp = await client.post(EXTRACT_API_URL, files=files)
if resp.status_code != 200:
raise HTTPException(500, "Extraction service error")
raw_list = resp.json()
normalized = [normalize_item(item) for item in raw_list]
# 3) persist into extracted_items
order_id = file.filename
records = [
(order_id,
idx,
it["Request Item"],
it["Quantity"],
it["Unit Price"],
it["Total Amount"])
for idx, it in enumerate(normalized)
]
await pool.executemany(
"""
INSERT INTO extracted_items
(order_id, line_idx, request_item, quantity, unit_price, total_amount)
VALUES($1,$2,$3,$4,$5,$6)
""",
records
)
return normalized
@app.post("/save-draft")
async def save_draft(payload: dict = Body(...)):
# existing file snapshot
order_id = payload.get("order_id", uuid.uuid4().hex)
items = payload.get("items", [])
path = os.path.join(SAVE_DIR, f"{order_id}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(items, f, ensure_ascii=False, indent=2)
# now also persist into order_drafts
records = [
(order_id,
idx,
it["Request Item"],
it["Quantity"],
it["Unit Price"],
it["Total Amount"])
for idx, it in enumerate(items)
]
await pool.executemany(
"""
INSERT INTO order_drafts
(order_id, line_idx, request_item, quantity, unit_price, total_amount)
VALUES($1,$2,$3,$4,$5,$6)
""",
records
)
return {"status": "ok", "file": path}
@app.post("/match")
async def match_items(
queries: list[str] = Body(..., embed=True, description="List of item descriptions to match")
):
"""
Query the 5 best match catalog items
"""
params = {"limit": 5}
payload = {"queries": queries}
async with httpx.AsyncClient() as client:
resp = await client.post(MATCH_API_URL, params=params, json=payload)
if resp.status_code != 200:
raise HTTPException(status_code=500, detail="Matching service error")
return resp.json()
@app.get("/catalog/search")
async def catalog_search(
q: str = Query(..., min_length=1, description="Search term"),
limit: int = Query(10, description="Max number of results"),
):
"""
return up to `limit` catalog entries containing substring `q`
"""
ql = q.lower()
results = [name for name in CATALOG if ql in name.lower()][:limit]
return {"results": results}
@app.post("/save-final")
async def save_final(
payload: dict = Body(
...,
example={
"order_id": "current_order",
"items": [
{
"Request Item": "Titanium Washer M4 30mm …",
"Match Item": "Titanium Washer M4 30mm …",
"Quantity": 25,
"Unit Price": 90.866,
"Total Amount": 2271.65
},
# …
],
},
)
):
"""
Persist the final chosen match for each line into matched_items table.
"""
order_id = payload.get("order_id", uuid.uuid4().hex)
items = payload.get("items", [])
if not isinstance(items, list):
raise HTTPException(status_code=400, detail="`items` must be a list")
records = []
for idx, it in enumerate(items):
records.append((
order_id,
idx,
it["Request Item"],
it["Match Item"],
it["Quantity"],
it["Unit Price"],
it["Total Amount"],
))
await pool.executemany(
"""
INSERT INTO matched_items
(order_id, line_idx, request_item, match_item, quantity, unit_price, total_amount)
VALUES($1,$2,$3,$4,$5,$6,$7)
""",
records
)
return {"status": "ok", "order_id": order_id}