|
| 1 | +"""Minimal FastAPI service for parsing/validation (US + optional international via libpostal).""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +from fastapi import FastAPI, HTTPException, Query |
| 8 | + |
| 9 | +from ryandata_address_utils.service import AddressService, parse |
| 10 | + |
| 11 | +try: |
| 12 | + from postal.parser import parse_address as lp_parse_address |
| 13 | +except ImportError: |
| 14 | + lp_parse_address = None |
| 15 | + |
| 16 | +app = FastAPI(title="RyanData Address Utils API", version="0.3.1") |
| 17 | +service = AddressService() |
| 18 | + |
| 19 | + |
| 20 | +@app.get("/health") |
| 21 | +def health() -> dict[str, str]: |
| 22 | + return {"status": "ok"} |
| 23 | + |
| 24 | + |
| 25 | +@app.get("/parse") |
| 26 | +def parse_us_address( |
| 27 | + address: str = Query(..., min_length=3), |
| 28 | + validate: bool = True, |
| 29 | +) -> dict[str, Any]: |
| 30 | + """Parse a US address using the standard service.""" |
| 31 | + result = parse(address, validate=validate) |
| 32 | + return { |
| 33 | + "is_valid": result.is_valid, |
| 34 | + "is_parsed": result.is_parsed, |
| 35 | + "address": result.to_dict() if result.address else None, |
| 36 | + "errors": [e.message for e in (result.validation.errors if result.validation else [])] |
| 37 | + if result.validation |
| 38 | + else [], |
| 39 | + } |
| 40 | + |
| 41 | + |
| 42 | +@app.get("/parse_international") |
| 43 | +def parse_international(address: str = Query(..., min_length=3)) -> dict[str, Any]: |
| 44 | + """Parse an international address via libpostal, if available.""" |
| 45 | + if lp_parse_address is None: |
| 46 | + raise HTTPException(status_code=501, detail="libpostal not available in this environment") |
| 47 | + |
| 48 | + parsed = lp_parse_address(address) |
| 49 | + # Convert list of (component, label) tuples into a dict of lists to preserve duplicates |
| 50 | + components: dict[str, list[str]] = {} |
| 51 | + for value, label in parsed: |
| 52 | + components.setdefault(label, []).append(value) |
| 53 | + |
| 54 | + return {"address": address, "components": components} |
| 55 | + |
| 56 | + |
| 57 | +@app.get("/parse_auto") |
| 58 | +def parse_auto(address: str = Query(..., min_length=3), validate: bool = True) -> dict[str, Any]: |
| 59 | + """Auto route: try US parser first; if it fails and libpostal is available, fall back.""" |
| 60 | + us_result = parse(address, validate=validate) |
| 61 | + if us_result.is_valid: |
| 62 | + return { |
| 63 | + "mode": "us", |
| 64 | + "is_valid": True, |
| 65 | + "is_parsed": True, |
| 66 | + "address": us_result.to_dict(), |
| 67 | + "errors": [], |
| 68 | + } |
| 69 | + |
| 70 | + if lp_parse_address is None: |
| 71 | + return { |
| 72 | + "mode": "us", |
| 73 | + "is_valid": False, |
| 74 | + "is_parsed": False, |
| 75 | + "errors": ["US parse failed and libpostal is not available"], |
| 76 | + } |
| 77 | + |
| 78 | + parsed = lp_parse_address(address) |
| 79 | + components: dict[str, list[str]] = {} |
| 80 | + for value, label in parsed: |
| 81 | + components.setdefault(label, []).append(value) |
| 82 | + |
| 83 | + return {"mode": "international", "is_valid": True, "is_parsed": True, "components": components} |
| 84 | + |
| 85 | + |
| 86 | +# To run: uvicorn ryandata_address_utils.api:app --host 0.0.0.0 --port 8000 |
0 commit comments