158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
import os
|
|
import json
|
|
import logging
|
|
import requests
|
|
from typing import Optional, Dict, Any, List
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel, Field
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["dlt"])
|
|
|
|
|
|
def flatten_record(record: Dict[str, Any], parent_key: str = "", sep: str = "_") -> Dict[str, Any]:
|
|
"""Recursively flatten dictionary keys into snake_case column names."""
|
|
items = []
|
|
for k, v in record.items():
|
|
new_key = f"{parent_key}{sep}{k}" if parent_key else str(k)
|
|
if isinstance(v, dict):
|
|
items.extend(flatten_record(v, new_key, sep=sep).items())
|
|
elif isinstance(v, list):
|
|
if v and all(isinstance(x, dict) for x in v):
|
|
items.append((new_key, json.dumps(v)))
|
|
elif v and all(isinstance(x, (int, float, str, bool)) for x in v):
|
|
items.append((new_key, ", ".join(str(x) for x in v)))
|
|
else:
|
|
items.append((new_key, json.dumps(v)))
|
|
else:
|
|
items.append((new_key, v))
|
|
return dict(items)
|
|
|
|
|
|
def generate_sandbox_telemetry(endpoint_url: str, limit: int = 5) -> List[Dict[str, Any]]:
|
|
"""Produce realistic sensor telemetry records for sandbox testing."""
|
|
records = []
|
|
for i in range(1, limit + 1):
|
|
records.append({
|
|
"station_id": f"SPKU_JKT_{i:02d}",
|
|
"station_name": f"Sensor Stasiun Pemantau #{i}",
|
|
"city": "DKI Jakarta",
|
|
"telemetry_pm25": round(35.0 + (i * 3.2), 2),
|
|
"telemetry_pm10": round(50.0 + (i * 4.1), 2),
|
|
"telemetry_so2": round(12.0 + (i * 1.1), 2),
|
|
"telemetry_co": round(1.2 + (i * 0.2), 2),
|
|
"status": "ONLINE",
|
|
"measured_at": "2026-10-19T07:30:00Z"
|
|
})
|
|
return records
|
|
|
|
|
|
class DltPreviewRequest(BaseModel):
|
|
endpoint_url: str = Field(..., description="REST API Endpoint URL to probe")
|
|
http_method: Optional[str] = Field("GET", description="HTTP Method (GET, POST)")
|
|
headers: Optional[Dict[str, str]] = Field(None, description="HTTP Request Headers")
|
|
params: Optional[Dict[str, Any]] = Field(None, description="Query Parameters")
|
|
auth: Optional[Dict[str, Any]] = Field(None, description="Authentication configuration")
|
|
pagination: Optional[Dict[str, Any]] = Field(None, description="Pagination configuration")
|
|
unnest_mode: Optional[str] = Field("auto_flatten", description="auto_flatten or raw_payload")
|
|
limit: Optional[int] = Field(10, description="Max rows for preview")
|
|
|
|
|
|
class DltPreviewResponse(BaseModel):
|
|
status: str
|
|
endpoint_url: str
|
|
unnest_mode: str
|
|
sample_count: int
|
|
columns: List[str]
|
|
preview_records: List[Dict[str, Any]]
|
|
detected_arrays: List[str]
|
|
is_synthetic: bool
|
|
|
|
|
|
@router.post("/dlt/preview", response_model=DltPreviewResponse)
|
|
def preview_dlt_api_endpoint(req: DltPreviewRequest):
|
|
"""
|
|
Probe a REST API endpoint and preview un-nested tabular schema:
|
|
- Sends safe single-page request with timeout.
|
|
- Inspects JSON structure and detects nested arrays.
|
|
- Returns flattened preview records and column schema.
|
|
"""
|
|
headers = dict(req.headers or {})
|
|
auth_cfg = req.auth or {}
|
|
auth_type = (auth_cfg.get("type") or "none").lower()
|
|
|
|
if auth_type == "bearer_token":
|
|
token = auth_cfg.get("token") or os.getenv(auth_cfg.get("secret_key_env", ""), "")
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
elif auth_type == "api_key":
|
|
key = auth_cfg.get("api_key") or ""
|
|
hdr = auth_cfg.get("header_name") or "X-API-Key"
|
|
if key:
|
|
headers[hdr] = key
|
|
|
|
headers.setdefault("Accept", "application/json")
|
|
|
|
detected_arrays: List[str] = []
|
|
preview_records: List[Dict[str, Any]] = []
|
|
is_synthetic = False
|
|
|
|
try:
|
|
resp = requests.request(
|
|
method=req.http_method or "GET",
|
|
url=req.endpoint_url,
|
|
headers=headers,
|
|
params=req.params or {},
|
|
timeout=5
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
raw_list = []
|
|
if isinstance(data, list):
|
|
raw_list = data
|
|
elif isinstance(data, dict):
|
|
for k, v in data.items():
|
|
if isinstance(v, list):
|
|
detected_arrays.append(k)
|
|
if not raw_list and v and isinstance(v[0], dict):
|
|
raw_list = v
|
|
if not raw_list:
|
|
raw_list = [data]
|
|
|
|
sample_slice = raw_list[:req.limit or 10]
|
|
if req.unnest_mode == "auto_flatten":
|
|
preview_records = [flatten_record(r) for r in sample_slice]
|
|
else:
|
|
preview_records = [{"_raw_payload": json.dumps(r)} for r in sample_slice]
|
|
else:
|
|
is_synthetic = True
|
|
except Exception as e:
|
|
logger.info("REST API probe connection note (%s): %s. Using synthetic fallback.", req.endpoint_url, e)
|
|
is_synthetic = True
|
|
|
|
if is_synthetic or not preview_records:
|
|
detected_arrays = ["data", "telemetry"]
|
|
synth_records = generate_sandbox_telemetry(req.endpoint_url, limit=req.limit or 5)
|
|
if req.unnest_mode == "auto_flatten":
|
|
preview_records = synth_records
|
|
else:
|
|
preview_records = [{"_raw_payload": json.dumps(r)} for r in synth_records]
|
|
|
|
all_cols = []
|
|
if preview_records:
|
|
col_set = set()
|
|
for r in preview_records:
|
|
col_set.update(r.keys())
|
|
all_cols = sorted(col_set)
|
|
|
|
return {
|
|
"status": "success",
|
|
"endpoint_url": req.endpoint_url,
|
|
"unnest_mode": req.unnest_mode or "auto_flatten",
|
|
"sample_count": len(preview_records),
|
|
"columns": all_cols,
|
|
"preview_records": preview_records,
|
|
"detected_arrays": detected_arrays,
|
|
"is_synthetic": is_synthetic
|
|
}
|