Spaces:
Runtime error
Runtime error
Download app.py from vishakhad30/Packing_List: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/spaces/vishakhad30/Packing_List/resolve/main/app.py
- Command line
-
hf download hf://spaces/vishakhad30/Packing_List/app.py
-
curl -L -o app.py https://huggingface.co/spaces/vishakhad30/Packing_List/resolve/main/app.py
10.5 kB
| import os | |
| import re | |
| import tempfile | |
| from pathlib import Path | |
| import gradio as gr | |
| import pandas as pd | |
| from pypdf import PdfReader | |
| from parsers.systemax import parse as parse_systemax | |
| from parsers.home_depot import parse as parse_home_depot | |
| from parsers.staples_com import parse as parse_staples_com | |
| from parsers.staples_advantage import parse as parse_staples_advantage | |
| from parsers.walmart import parse as parse_walmart | |
| from parsers.lowes import parse as parse_lowes | |
| from parsers.quill import parse as parse_quill | |
| from parsers.msc_industrial import parse as parse_msc_industrial | |
| from parsers.lowes_rona import parse as parse_lowes_rona | |
| from parsers.united_stationers import parse as parse_united_stationers | |
| from parsers.bh_photo_video import parse as parse_bh_photo_video | |
| HEADERS = ["Trading Partner", "PO Number", "Ship Via", "Order Date", "Source PDF"] | |
| def normalize(text): | |
| text = text.replace("\x00", " ") | |
| text = text.replace("\r", "\n") | |
| text = re.sub(r"[ \t]+", " ", text) | |
| text = re.sub(r"\n[ \t]+", "\n", text) | |
| text = re.sub(r"\n{3,}", "\n\n", text) | |
| return text.strip() | |
| def read_pdf(path): | |
| reader = PdfReader(path) | |
| pages = [] | |
| for page in reader.pages: | |
| try: | |
| pages.append(page.extract_text() or "") | |
| except Exception: | |
| pages.append("") | |
| return "\n\n".join(pages) | |
| def identify_vendor(filename): | |
| n = re.sub(r"[^a-z0-9]", "", Path(filename).stem.lower()) | |
| # More specific names must be checked before generic substrings | |
| # (e.g. "lowesrona" before "lowes"). | |
| if "systemax" in n: | |
| return "SYSTEMAX" | |
| if "homedepot" in n: | |
| return "HOME DEPOT" | |
| if "staplesadvantage" in n: | |
| return "STAPLES ADVANTAGE" | |
| if "staplescom" in n: | |
| return "STAPLES.COM" | |
| if "walmartcomdirecttocustomer" in n or "walmart" in n: | |
| return "WALMART" | |
| if "lowesrona" in n or "ronaps" in n or ("rona" in n and "lowes" in n): | |
| return "LOWE'S / RONA" | |
| if "lowessos" in n or "lowes" in n: | |
| return "LOWE'S" | |
| if "quillcom" in n or "quill" in n: | |
| return "QUILL" | |
| if "mscindustrial" in n or n.startswith("msc"): | |
| return "MSC INDUSTRIAL SUPPLY" | |
| if "unitedstationers" in n or "essendant" in n: | |
| return "UNITED STATIONERS" | |
| if "bandhphotovideo" in n or "bhphotovideo" in n: | |
| return "B&H PHOTO-VIDEO" | |
| return "UNKNOWN" | |
| PARSERS = { | |
| "SYSTEMAX": parse_systemax, | |
| "HOME DEPOT": parse_home_depot, | |
| "STAPLES.COM": parse_staples_com, | |
| "STAPLES ADVANTAGE": parse_staples_advantage, | |
| "WALMART": parse_walmart, | |
| "LOWE'S": parse_lowes, | |
| "QUILL": parse_quill, | |
| "MSC INDUSTRIAL SUPPLY": parse_msc_industrial, | |
| "LOWE'S / RONA": parse_lowes_rona, | |
| "UNITED STATIONERS": parse_united_stationers, | |
| "B&H PHOTO-VIDEO": parse_bh_photo_video, | |
| } | |
| def process_one(path): | |
| filename = Path(path).name | |
| vendor = identify_vendor(filename) | |
| if vendor == "UNKNOWN": | |
| return [], f"⚠️ UNKNOWN VENDOR: {filename}" | |
| raw = read_pdf(path) | |
| text = normalize(raw) | |
| if not text: | |
| return [], f"⚠️ NO TEXT EXTRACTED: {filename}" | |
| rows = PARSERS[vendor](text) | |
| clean = [] | |
| for row in rows: | |
| row = { | |
| "Trading Partner": vendor, | |
| "PO Number": str(row.get("PO Number", "")).strip(), | |
| "Ship Via": str(row.get("Ship Via", "")).strip(), | |
| "Order Date": str(row.get("Order Date", "")).strip(), | |
| "Source PDF": filename, | |
| } | |
| # Never allow obvious non-PO text into the PO column. | |
| po = row["PO Number"] | |
| if po and ( | |
| len(po) > 40 | |
| or "direct inquiries" in po.lower() | |
| or "customer service" in po.lower() | |
| or po.lower() in { | |
| "services", "service", "customer", "sales", "date", | |
| "location", "salesperson", "release", "order", | |
| } | |
| ): | |
| row["PO Number"] = "" | |
| # Reject dates mistakenly captured as Ship Via. | |
| ship = row["Ship Via"] | |
| if ship and re.fullmatch(r"\d{1,2}/\d{1,2}/\d{2,4}", ship): | |
| if not row["Order Date"]: | |
| row["Order Date"] = ship | |
| row["Ship Via"] = "" | |
| if any(row[h] for h in HEADERS[:-1]): | |
| clean.append(row) | |
| return clean, f"✅ {vendor}: {len(clean)} record(s)" | |
| def process_files(files): | |
| if not files: | |
| return "Please upload one or more PDF files.", None | |
| all_rows = [] | |
| messages = [] | |
| for f in files: | |
| path = getattr(f, "name", f) | |
| try: | |
| rows, message = process_one(path) | |
| all_rows.extend(rows) | |
| messages.append(message) | |
| except Exception as e: | |
| messages.append(f"❌ {Path(path).name}: {type(e).__name__}: {e}") | |
| df = pd.DataFrame(all_rows, columns=HEADERS) | |
| # Keep leading zeros by storing PO values as strings. | |
| if not df.empty: | |
| df["PO Number"] = df["PO Number"].astype(str) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") | |
| tmp.close() | |
| df.to_excel(tmp.name, index=False, engine="openpyxl") | |
| summary = "\n".join(messages) | |
| summary += f"\n\nTOTAL RECORDS: {len(df)}" | |
| return summary, tmp.name | |
| THEME = gr.themes.Soft( | |
| primary_hue="rose", | |
| secondary_hue="teal", | |
| neutral_hue="stone", | |
| font=[gr.themes.GoogleFont("Nunito"), "ui-sans-serif", "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace"], | |
| ).set( | |
| body_background_fill="#f7f3ef", | |
| body_background_fill_dark="#f7f3ef", | |
| block_background_fill="#ffffff", | |
| block_border_width="0px", | |
| block_radius="18px", | |
| block_shadow="0 8px 28px rgba(120, 90, 90, 0.08)", | |
| button_primary_background_fill="#e86b7a", | |
| button_primary_background_fill_hover="#d95a6a", | |
| button_primary_text_color="#ffffff", | |
| button_large_radius="14px", | |
| input_radius="14px", | |
| ) | |
| APP_CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@500;600;700;800&display=swap'); | |
| .gradio-container { | |
| max-width: 1100px !important; | |
| margin: 0 auto !important; | |
| padding: 0.85rem 1rem 1rem !important; | |
| font-family: 'Nunito', ui-sans-serif, system-ui, sans-serif !important; | |
| } | |
| .main-wrap { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.7rem !important; | |
| } | |
| .hero-card { | |
| background: linear-gradient(135deg, #fff7f5 0%, #ffffff 55%, #f3fbf8 100%); | |
| border: 1px solid rgba(232, 107, 122, 0.18); | |
| border-radius: 18px; | |
| padding: 0.75rem 1.1rem !important; | |
| box-shadow: 0 10px 30px rgba(180, 110, 110, 0.08); | |
| } | |
| .hero-card h1 { | |
| font-size: 1.4rem !important; | |
| font-weight: 800 !important; | |
| letter-spacing: -0.02em; | |
| color: #3d2c2e !important; | |
| margin: 0 !important; | |
| } | |
| .hero-card p { | |
| display: none !important; | |
| } | |
| .panel { | |
| background: #ffffff; | |
| border-radius: 16px; | |
| padding: 0.65rem 0.75rem 0.55rem !important; | |
| box-shadow: 0 8px 24px rgba(120, 90, 90, 0.07); | |
| border: 1px solid rgba(80, 60, 60, 0.05); | |
| } | |
| .panel .label-wrap span, | |
| .panel label span { | |
| font-weight: 700 !important; | |
| color: #5c4548 !important; | |
| } | |
| button.primary, | |
| .primary { | |
| min-height: 42px !important; | |
| font-weight: 800 !important; | |
| letter-spacing: 0.02em !important; | |
| box-shadow: 0 8px 18px rgba(232, 107, 122, 0.28) !important; | |
| } | |
| /* Force true 50/50 side-by-side layout */ | |
| #main-split { | |
| display: grid !important; | |
| grid-template-columns: 1fr 1fr !important; | |
| gap: 0.75rem !important; | |
| align-items: stretch !important; | |
| } | |
| #main-split > div { | |
| min-width: 0 !important; | |
| width: 100% !important; | |
| max-width: none !important; | |
| flex: unset !important; | |
| } | |
| #upload-col { | |
| height: 100% !important; | |
| } | |
| #upload-col .wrap, | |
| #upload-col [data-testid="file"] { | |
| height: 100% !important; | |
| } | |
| #upload-col .file-preview-holder, | |
| #upload-col .file-preview, | |
| #upload-col ul, | |
| #upload-col .scroll-hide { | |
| max-height: 260px !important; | |
| overflow-y: auto !important; | |
| } | |
| #side-col { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| gap: 0.7rem !important; | |
| height: 100% !important; | |
| min-height: 340px !important; | |
| } | |
| #side-col > div { | |
| flex: 1 1 0 !important; | |
| min-height: 160px !important; | |
| } | |
| #status-box textarea { | |
| min-height: 140px !important; | |
| height: 140px !important; | |
| max-height: 180px !important; | |
| overflow-y: auto !important; | |
| resize: none !important; | |
| } | |
| #download-box, | |
| #download-box .wrap, | |
| #download-box [data-testid="file"] { | |
| min-height: 140px !important; | |
| height: 140px !important; | |
| } | |
| @media (max-width: 760px) { | |
| #main-split { | |
| grid-template-columns: 1fr !important; | |
| } | |
| } | |
| footer { display: none !important; } | |
| """ | |
| with gr.Blocks(title="Packing List PDF Extractor") as demo: | |
| with gr.Column(elem_classes=["main-wrap"]): | |
| gr.Markdown("# Packing List PDF Extractor", elem_classes=["hero-card"]) | |
| process = gr.Button("Process PDFs", variant="primary", size="lg") | |
| with gr.Row(elem_id="main-split"): | |
| with gr.Column(elem_id="upload-col", elem_classes=["panel"]): | |
| files = gr.File( | |
| label="Upload PDFs", | |
| file_count="multiple", | |
| file_types=[".pdf"], | |
| type="filepath", | |
| height=340, | |
| ) | |
| with gr.Column(elem_id="side-col"): | |
| with gr.Column(elem_classes=["panel"]): | |
| status = gr.Textbox( | |
| label="Status", | |
| lines=7, | |
| max_lines=9, | |
| elem_id="status-box", | |
| ) | |
| with gr.Column(elem_classes=["panel"]): | |
| download = gr.File( | |
| label="Excel download", | |
| height=140, | |
| elem_id="download-box", | |
| ) | |
| clear = gr.Button("Clear form", variant="secondary", size="lg") | |
| def clear_form(): | |
| return None, "", None | |
| process.click( | |
| fn=process_files, | |
| inputs=files, | |
| outputs=[status, download], | |
| ) | |
| clear.click( | |
| fn=clear_form, | |
| inputs=None, | |
| outputs=[files, status, download], | |
| ) | |
| if __name__ == "__main__": | |
| # HF Spaces needs 0.0.0.0; locally bind to localhost so the URL is usable. | |
| on_spaces = bool(os.getenv("SPACE_ID") or os.getenv("SYSTEM") == "spaces") | |
| demo.launch( | |
| server_name="0.0.0.0" if on_spaces else "127.0.0.1", | |
| server_port=7860, | |
| ssr_mode=False, | |
| theme=THEME, | |
| css=APP_CSS, | |
| ) | |