Repository navigation
Expand file tree
/
Copy pathfastapi_clean_service.py
More file actions
176 lines (138 loc) · 4.62 KB
/
Copy pathfastapi_clean_service.py
File metadata and controls
176 lines (138 loc) · 4.62 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
"""examples/integrations/fastapi_clean_service.py — FastAPI Cleaning Service.
Problem:
--------
Production systems often need to expose data-cleaning workflows through HTTP
APIs. Running CPU-bound dataframe cleaning directly inside an async FastAPI
endpoint can block the event loop.
This example provides a lightweight FastAPI service that accepts either JSON
tabular records or CSV uploads and runs FreshData cleaning in a worker thread.
Installation:
-------------
pip install fastapi uvicorn python-multipart "freshdata-cleaner[dev]"
"""
from __future__ import annotations
import asyncio
from io import BytesIO
from typing import Any
import pandas as pd
from fastapi import FastAPI, HTTPException, Request
from starlette.datastructures import UploadFile
import freshdata as fd
app = FastAPI(
title="FreshData Cleaning Service",
description="Asynchronous FastAPI integration for FreshData.",
)
def parse_bool(value: object, default: bool = False) -> bool:
"""Parse common boolean representations from form or JSON input."""
if value is None:
return default
if isinstance(value, bool):
return value
return str(value).strip().lower() in {"1", "true", "yes", "on"}
async def clean_dataframe(
df: pd.DataFrame,
*,
strategy: object = "balanced",
impute: object | None = None,
drop_duplicates: bool = False,
) -> tuple[pd.DataFrame, Any]:
"""Run FreshData cleaning without blocking the FastAPI event loop."""
clean_options: dict[str, object] = {
"strategy": strategy,
"drop_duplicates": drop_duplicates,
"return_report": True,
}
if impute is not None:
clean_options["impute"] = impute
cleaned_df, report = await asyncio.to_thread(
fd.clean,
df,
**clean_options,
)
return cleaned_df, report
@app.post("/clean")
async def clean_data(request: Request) -> dict[str, object]:
"""Clean JSON tabular records or an uploaded CSV file."""
content_type = request.headers.get("content-type", "")
if "application/json" in content_type:
try:
payload = await request.json()
except ValueError as exc:
raise HTTPException(
status_code=400,
detail="Invalid JSON body.",
) from exc
if not isinstance(payload, dict):
raise HTTPException(
status_code=400,
detail="JSON body must be an object.",
)
records = payload.get("records")
if not isinstance(records, list) or not records:
raise HTTPException(
status_code=400,
detail="JSON body must include non-empty 'records'.",
)
try:
df = pd.DataFrame(records)
except Exception as exc:
raise HTTPException(
status_code=400,
detail="Unable to convert records into tabular data.",
) from exc
strategy = payload.get("strategy", "balanced")
impute = payload.get("impute")
drop_duplicates = parse_bool(
payload.get("drop_duplicates"),
default=False,
)
elif "multipart/form-data" in content_type:
form = await request.form()
file = form.get("file")
if not isinstance(file, UploadFile):
raise HTTPException(
status_code=400,
detail="CSV file upload is required.",
)
contents = await file.read()
try:
df = pd.read_csv(BytesIO(contents))
except Exception as exc:
raise HTTPException(
status_code=400,
detail="Unable to read CSV file.",
) from exc
strategy = form.get("strategy", "balanced")
impute = form.get("impute")
drop_duplicates = parse_bool(
form.get("drop_duplicates"),
default=False,
)
else:
raise HTTPException(
status_code=415,
detail="Use application/json or multipart/form-data.",
)
try:
cleaned_df, report = await clean_dataframe(
df,
strategy=strategy,
impute=impute,
drop_duplicates=drop_duplicates,
)
except Exception as exc:
raise HTTPException(
status_code=422,
detail=f"FreshData cleaning failed: {exc}",
) from exc
return {
"data": cleaned_df.to_dict(orient="records"),
"report": report.to_dict(),
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
app,
host="127.0.0.1",
port=8000,
)