Soumik Bose commited on
Commit ·
c0fce26
1
Parent(s): 65fe3da
commit
Browse files- controller.py +11 -0
- extract_csv_metadata_service.py +92 -0
- table_info_model.py +18 -0
controller.py
CHANGED
|
@@ -37,12 +37,14 @@ import uvicorn
|
|
| 37 |
from csv_analysis_service import execute_analysis_logic
|
| 38 |
from csv_chart_service import execute_python_code
|
| 39 |
from csv_metadata_service import CsvDataRequest, CsvInfoRequest, CsvInfoResponse, PythonExecutionRequest, PythonExecutionResponse, execute_python_logic, get_csv_basic_info, get_robust_csv_rows
|
|
|
|
| 40 |
from mongo_service import convert_oid, execute_mongo_operation, extract_database_name, sanitize_json_input
|
| 41 |
from pydantic_csv_analysis_model import AnalysisRequest, AnalysisResponse
|
| 42 |
from pydantic_csv_charts_model import ChartExecutionPayload, ChartExecutionResponse
|
| 43 |
from pydantic_mongo_executor_model import ExecutorPayload, ExecutorResponse
|
| 44 |
from report_service import FileBoxProps, ReportRequest, execute_report_generation
|
| 45 |
from supabase_service import upload_bytes_to_supabase
|
|
|
|
| 46 |
|
| 47 |
# --- Configuration & Setup ---
|
| 48 |
load_dotenv()
|
|
@@ -631,6 +633,15 @@ async def root():
|
|
| 631 |
async def ping():
|
| 632 |
return {"message": "I am alive!"}
|
| 633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
if __name__ == "__main__":
|
| 635 |
host = os.getenv("HOST", "0.0.0.0")
|
| 636 |
port = int(os.getenv("PORT", 7860))
|
|
|
|
| 37 |
from csv_analysis_service import execute_analysis_logic
|
| 38 |
from csv_chart_service import execute_python_code
|
| 39 |
from csv_metadata_service import CsvDataRequest, CsvInfoRequest, CsvInfoResponse, PythonExecutionRequest, PythonExecutionResponse, execute_python_logic, get_csv_basic_info, get_robust_csv_rows
|
| 40 |
+
from extract_csv_metadata_service import extract_csv_metadata_logic
|
| 41 |
from mongo_service import convert_oid, execute_mongo_operation, extract_database_name, sanitize_json_input
|
| 42 |
from pydantic_csv_analysis_model import AnalysisRequest, AnalysisResponse
|
| 43 |
from pydantic_csv_charts_model import ChartExecutionPayload, ChartExecutionResponse
|
| 44 |
from pydantic_mongo_executor_model import ExecutorPayload, ExecutorResponse
|
| 45 |
from report_service import FileBoxProps, ReportRequest, execute_report_generation
|
| 46 |
from supabase_service import upload_bytes_to_supabase
|
| 47 |
+
from table_info_model import CsvFieldsRequest, CsvFieldsResponse
|
| 48 |
|
| 49 |
# --- Configuration & Setup ---
|
| 50 |
load_dotenv()
|
|
|
|
| 633 |
async def ping():
|
| 634 |
return {"message": "I am alive!"}
|
| 635 |
|
| 636 |
+
@app.post("/api/get_csv_fields", response_model=CsvFieldsResponse)
|
| 637 |
+
async def get_csv_fields_endpoint(payload: CsvFieldsRequest, token: str = Depends(validate_token)):
|
| 638 |
+
request_id = str(uuid.uuid4())[:8]
|
| 639 |
+
try:
|
| 640 |
+
fields = await run_in_threadpool(extract_csv_metadata_logic, csv_url=payload.csv_url)
|
| 641 |
+
return CsvFieldsResponse(success=True, fields=fields, request_id=request_id)
|
| 642 |
+
except Exception as e:
|
| 643 |
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e), "request_id": request_id})
|
| 644 |
+
|
| 645 |
if __name__ == "__main__":
|
| 646 |
host = os.getenv("HOST", "0.0.0.0")
|
| 647 |
port = int(os.getenv("PORT", 7860))
|
extract_csv_metadata_service.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import List, Dict, Any, Optional
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
# Configure Logging
|
| 7 |
+
logging.basicConfig(
|
| 8 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 9 |
+
level=logging.INFO
|
| 10 |
+
)
|
| 11 |
+
logger = logging.getLogger("csv_metadata_service")
|
| 12 |
+
|
| 13 |
+
def extract_csv_metadata_logic(csv_url: str) -> List[Dict[str, Any]]:
|
| 14 |
+
"""
|
| 15 |
+
Analyzes a CSV file from a URL to infer column metadata.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
csv_url (str): The public URL of the CSV file.
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
List[Dict[str, Any]]: A list of dictionaries containing field metadata.
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
logger.info(f"Extracting metadata from: {csv_url}")
|
| 25 |
+
|
| 26 |
+
# Load a sample of the CSV to infer types (first 50 rows is usually enough)
|
| 27 |
+
# Using on_bad_lines='skip' to be robust against malformed rows
|
| 28 |
+
# storage_options={'User-Agent': ...} can be added if requests are blocked
|
| 29 |
+
df = pd.read_csv(csv_url, nrows=50, on_bad_lines='skip')
|
| 30 |
+
|
| 31 |
+
fields = []
|
| 32 |
+
|
| 33 |
+
for col in df.columns:
|
| 34 |
+
dtype = df[col].dtype
|
| 35 |
+
|
| 36 |
+
# 1. Default type
|
| 37 |
+
generic_type = 'string'
|
| 38 |
+
|
| 39 |
+
# 2. Map Pandas/Numpy types to generic keys
|
| 40 |
+
if pd.api.types.is_integer_dtype(dtype):
|
| 41 |
+
generic_type = 'integer'
|
| 42 |
+
elif pd.api.types.is_float_dtype(dtype):
|
| 43 |
+
generic_type = 'decimal'
|
| 44 |
+
elif pd.api.types.is_bool_dtype(dtype):
|
| 45 |
+
generic_type = 'boolean'
|
| 46 |
+
elif pd.api.types.is_datetime64_any_dtype(dtype):
|
| 47 |
+
generic_type = 'date'
|
| 48 |
+
else:
|
| 49 |
+
# 3. Attempt to detect dates in object/string columns
|
| 50 |
+
# We check the first non-null value to see if it parses as a date
|
| 51 |
+
if len(df) > 0:
|
| 52 |
+
try:
|
| 53 |
+
first_valid = df[col].dropna().iloc[0] if not df[col].dropna().empty else ""
|
| 54 |
+
val_str = str(first_valid).strip()
|
| 55 |
+
|
| 56 |
+
# Simple check to avoid treating plain numbers as dates
|
| 57 |
+
if val_str and not val_str.isdigit():
|
| 58 |
+
pd.to_datetime(val_str)
|
| 59 |
+
generic_type = 'date'
|
| 60 |
+
except (ValueError, TypeError):
|
| 61 |
+
# Not a date, keep as string
|
| 62 |
+
pass
|
| 63 |
+
|
| 64 |
+
# 4. Determine Nullability
|
| 65 |
+
is_nullable = bool(df[col].isnull().any())
|
| 66 |
+
|
| 67 |
+
# 5. Heuristic for Primary Key
|
| 68 |
+
lower_name = col.lower()
|
| 69 |
+
potential_names = ['id', 'uuid', '_id', 'pk']
|
| 70 |
+
|
| 71 |
+
# It's likely a PK if it has a common ID name OR ends in _id
|
| 72 |
+
is_pk_name = lower_name in potential_names or lower_name.endswith('_id')
|
| 73 |
+
|
| 74 |
+
# It must also be unique in our sample
|
| 75 |
+
is_unique = df[col].is_unique if not df.empty else False
|
| 76 |
+
|
| 77 |
+
is_primary_key = is_pk_name and is_unique
|
| 78 |
+
|
| 79 |
+
fields.append({
|
| 80 |
+
"name": col,
|
| 81 |
+
"type": generic_type,
|
| 82 |
+
"nullable": is_nullable,
|
| 83 |
+
"isPrimaryKey": is_primary_key
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
logger.info(f"Successfully extracted {len(fields)} fields.")
|
| 87 |
+
return fields
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logger.error(f"CSV Analysis Error for {csv_url}: {str(e)}")
|
| 91 |
+
# Re-raise to be handled by the controller's exception handler
|
| 92 |
+
raise e
|
table_info_model.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class CsvFieldsRequest(BaseModel):
|
| 6 |
+
csv_url: str
|
| 7 |
+
|
| 8 |
+
class FieldMetadata(BaseModel):
|
| 9 |
+
name: str
|
| 10 |
+
type: str
|
| 11 |
+
nullable: bool
|
| 12 |
+
isPrimaryKey: bool
|
| 13 |
+
|
| 14 |
+
class CsvFieldsResponse(BaseModel):
|
| 15 |
+
success: bool
|
| 16 |
+
fields: List[FieldMetadata]
|
| 17 |
+
error: Optional[str] = None
|
| 18 |
+
request_id: str
|