Files
planet/backend/app/api/v1/datasource_config.py
2026-03-05 11:46:58 +08:00

310 lines
9.5 KiB
Python

"""DataSourceConfig API for user-defined data sources"""
from typing import Optional
from datetime import datetime
import base64
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, Field
import httpx
from app.db.session import get_db
from app.models.user import User
from app.models.datasource_config import DataSourceConfig
from app.core.security import get_current_user
from app.core.cache import cache
router = APIRouter()
class DataSourceConfigCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
source_type: str = Field(..., description="http, api, database")
endpoint: str = Field(..., max_length=500)
auth_type: str = Field(default="none", description="none, bearer, api_key, basic")
auth_config: dict = Field(default={})
headers: dict = Field(default={})
config: dict = Field(default={"timeout": 30, "retry": 3})
class DataSourceConfigUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = None
source_type: Optional[str] = None
endpoint: Optional[str] = Field(None, max_length=500)
auth_type: Optional[str] = None
auth_config: Optional[dict] = None
headers: Optional[dict] = None
config: Optional[dict] = None
is_active: Optional[bool] = None
class DataSourceConfigResponse(BaseModel):
id: int
name: str
description: Optional[str]
source_type: str
endpoint: str
auth_type: str
headers: dict
config: dict
is_active: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
async def test_endpoint(
endpoint: str,
auth_type: str,
auth_config: dict,
headers: dict,
config: dict,
) -> dict:
"""Test an endpoint connection"""
timeout = config.get("timeout", 30)
test_headers = headers.copy()
# Add auth headers
if auth_type == "bearer" and auth_config.get("token"):
test_headers["Authorization"] = f"Bearer {auth_config['token']}"
elif auth_type == "api_key" and auth_config.get("api_key"):
key_name = auth_config.get("key_name", "X-API-Key")
test_headers[key_name] = auth_config["api_key"]
elif auth_type == "basic":
username = auth_config.get("username", "")
password = auth_config.get("password", "")
credentials = f"{username}:{password}"
encoded = base64.b64encode(credentials.encode()).decode()
test_headers["Authorization"] = f"Basic {encoded}"
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(endpoint, headers=test_headers)
response.raise_for_status()
return {
"status_code": response.status_code,
"success": True,
"response_time_ms": response.elapsed.total_seconds() * 1000,
"data_preview": str(response.json()[:3])
if response.headers.get("content-type", "").startswith("application/json")
else response.text[:200],
}
@router.get("/configs")
async def list_configs(
active_only: bool = False,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List all user-defined data source configurations"""
query = select(DataSourceConfig)
if active_only:
query = query.where(DataSourceConfig.is_active == True)
query = query.order_by(DataSourceConfig.created_at.desc())
result = await db.execute(query)
configs = result.scalars().all()
return {
"total": len(configs),
"data": [
{
"id": c.id,
"name": c.name,
"description": c.description,
"source_type": c.source_type,
"endpoint": c.endpoint,
"auth_type": c.auth_type,
"headers": c.headers,
"config": c.config,
"is_active": c.is_active,
"created_at": c.created_at.isoformat() if c.created_at else None,
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
}
for c in configs
],
}
@router.get("/configs/{config_id}")
async def get_config(
config_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get a single data source configuration"""
result = await db.execute(select(DataSourceConfig).where(DataSourceConfig.id == config_id))
config = result.scalar_one_or_none()
if not config:
raise HTTPException(status_code=404, detail="Configuration not found")
return {
"id": config.id,
"name": config.name,
"description": config.description,
"source_type": config.source_type,
"endpoint": config.endpoint,
"auth_type": config.auth_type,
"auth_config": {}, # Don't return sensitive data
"headers": config.headers,
"config": config.config,
"is_active": config.is_active,
"created_at": config.created_at.isoformat() if config.created_at else None,
"updated_at": config.updated_at.isoformat() if config.updated_at else None,
}
@router.post("/configs")
async def create_config(
config_data: DataSourceConfigCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new data source configuration"""
config = DataSourceConfig(
name=config_data.name,
description=config_data.description,
source_type=config_data.source_type,
endpoint=config_data.endpoint,
auth_type=config_data.auth_type,
auth_config=config_data.auth_config,
headers=config_data.headers,
config=config_data.config,
)
db.add(config)
await db.commit()
await db.refresh(config)
cache.delete_pattern("datasource_configs:*")
return {
"id": config.id,
"name": config.name,
"message": "Configuration created successfully",
}
@router.put("/configs/{config_id}")
async def update_config(
config_id: int,
config_data: DataSourceConfigUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update a data source configuration"""
result = await db.execute(select(DataSourceConfig).where(DataSourceConfig.id == config_id))
config = result.scalar_one_or_none()
if not config:
raise HTTPException(status_code=404, detail="Configuration not found")
update_data = config_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(config, field, value)
await db.commit()
await db.refresh(config)
cache.delete_pattern("datasource_configs:*")
return {
"id": config.id,
"name": config.name,
"message": "Configuration updated successfully",
}
@router.delete("/configs/{config_id}")
async def delete_config(
config_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Delete a data source configuration"""
result = await db.execute(select(DataSourceConfig).where(DataSourceConfig.id == config_id))
config = result.scalar_one_or_none()
if not config:
raise HTTPException(status_code=404, detail="Configuration not found")
await db.delete(config)
await db.commit()
cache.delete_pattern("datasource_configs:*")
return {"message": "Configuration deleted successfully"}
@router.post("/configs/{config_id}/test")
async def test_config(
config_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Test a data source configuration"""
result = await db.execute(select(DataSourceConfig).where(DataSourceConfig.id == config_id))
config = result.scalar_one_or_none()
if not config:
raise HTTPException(status_code=404, detail="Configuration not found")
try:
result = await test_endpoint(
endpoint=config.endpoint,
auth_type=config.auth_type,
auth_config=config.auth_config or {},
headers=config.headers or {},
config=config.config or {},
)
return result
except httpx.HTTPStatusError as e:
return {
"success": False,
"error": f"HTTP Error: {e.response.status_code}",
"message": str(e),
}
except Exception as e:
return {
"success": False,
"error": "Connection failed",
"message": str(e),
}
@router.post("/configs/test")
async def test_new_config(
config_data: DataSourceConfigCreate,
current_user: User = Depends(get_current_user),
):
"""Test a new data source configuration without saving"""
try:
result = await test_endpoint(
endpoint=config_data.endpoint,
auth_type=config_data.auth_type,
auth_config=config_data.auth_config or {},
headers=config_data.headers or {},
config=config_data.config or {},
)
return result
except httpx.HTTPStatusError as e:
return {
"success": False,
"error": f"HTTP Error: {e.response.status_code}",
"message": str(e),
}
except Exception as e:
return {
"success": False,
"error": "Connection failed",
"message": str(e),
}