You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
7.7 KiB
Python
207 lines
7.7 KiB
Python
"""
|
|
Data migration endpoints
|
|
"""
|
|
|
|
from datetime import datetime
|
|
import time
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Header
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
import httpx
|
|
|
|
from app.core.database import get_db
|
|
from app.core.config import settings
|
|
from app.schemas.financial import MigrationRequest, MigrationResponse, ErrorType
|
|
from app.models.financial import FinancialData, CalculatedMetrics, Company
|
|
|
|
router = APIRouter()
|
|
|
|
async def verify_migration_key(x_api_key: Optional[str] = Header(None)):
|
|
"""Verify migration API key"""
|
|
if not settings.ALLOW_MIGRATION:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error_type": ErrorType.AUTHENTICATION_ERROR,
|
|
"message": "Migration endpoint is disabled"
|
|
}
|
|
)
|
|
|
|
if x_api_key != settings.MIGRATION_API_KEY:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error_type": ErrorType.AUTHENTICATION_ERROR,
|
|
"message": "Invalid migration API key"
|
|
}
|
|
)
|
|
|
|
@router.post(
|
|
"/migrate",
|
|
response_model=MigrationResponse,
|
|
summary="Migrate data from another instance",
|
|
description="""
|
|
Migrate financial data from another SEC Investment API instance.
|
|
|
|
This endpoint allows you to:
|
|
- Transfer all data from one instance to another
|
|
- Migrate specific tickers only
|
|
- Migrate data within specific date ranges
|
|
|
|
Requires valid migration API key in X-API-Key header.
|
|
"""
|
|
)
|
|
async def migrate_data(
|
|
request: MigrationRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: None = Depends(verify_migration_key)
|
|
):
|
|
"""Migrate data from another instance"""
|
|
|
|
start_time = time.time()
|
|
total_records = 0
|
|
migrated_records = 0
|
|
failed_records = 0
|
|
errors = []
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
try:
|
|
# Set up headers for source API
|
|
headers = {"X-API-Key": request.api_key}
|
|
|
|
# Get list of tickers to migrate
|
|
if request.tickers:
|
|
tickers = request.tickers
|
|
else:
|
|
# Get all tickers from source
|
|
response = await client.get(
|
|
f"{request.source_url}/api/v1/companies",
|
|
headers=headers
|
|
)
|
|
if response.status_code == 200:
|
|
companies = response.json()
|
|
tickers = [c["ticker"] for c in companies]
|
|
else:
|
|
raise ValueError("Failed to fetch company list from source")
|
|
|
|
# Migrate each ticker
|
|
for ticker in tickers:
|
|
try:
|
|
# Build query parameters
|
|
params = {"ticker": ticker}
|
|
if request.start_date:
|
|
params["start_date"] = request.start_date.isoformat()
|
|
if request.end_date:
|
|
params["end_date"] = request.end_date.isoformat()
|
|
|
|
# Fetch financial data
|
|
response = await client.get(
|
|
f"{request.source_url}/api/v1/financial/data/{ticker}",
|
|
headers=headers,
|
|
params=params
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
# Store company info
|
|
company_info = data.get("company", {})
|
|
if company_info:
|
|
company = await db.get(Company, ticker)
|
|
if not company:
|
|
company = Company(
|
|
ticker=ticker,
|
|
name=company_info.get("name"),
|
|
cik=company_info.get("cik"),
|
|
sector=company_info.get("sector"),
|
|
industry=company_info.get("industry"),
|
|
business_description=company_info.get("business_description")
|
|
)
|
|
db.add(company)
|
|
|
|
# Store financial data
|
|
for fd in data.get("financial_data", []):
|
|
total_records += 1
|
|
# Check if exists
|
|
existing = await db.get(
|
|
FinancialData,
|
|
(ticker, fd["period_date"], fd["period_type"])
|
|
)
|
|
if not existing:
|
|
fin_data = FinancialData(**fd, ticker=ticker)
|
|
db.add(fin_data)
|
|
migrated_records += 1
|
|
|
|
# Store calculated metrics
|
|
for cm in data.get("calculated_metrics", []):
|
|
total_records += 1
|
|
# Check if exists
|
|
existing = await db.get(
|
|
CalculatedMetrics,
|
|
(ticker, cm["calculation_date"], cm["period_date"])
|
|
)
|
|
if not existing:
|
|
metrics = CalculatedMetrics(**cm, ticker=ticker)
|
|
db.add(metrics)
|
|
migrated_records += 1
|
|
|
|
await db.commit()
|
|
|
|
else:
|
|
failed_records += 1
|
|
errors.append({
|
|
"ticker": ticker,
|
|
"error": f"HTTP {response.status_code}: {response.text}"
|
|
})
|
|
|
|
except Exception as e:
|
|
failed_records += 1
|
|
errors.append({
|
|
"ticker": ticker,
|
|
"error": str(e)
|
|
})
|
|
await db.rollback()
|
|
|
|
duration = time.time() - start_time
|
|
|
|
return MigrationResponse(
|
|
status="completed" if failed_records == 0 else "completed_with_errors",
|
|
total_records=total_records,
|
|
migrated_records=migrated_records,
|
|
failed_records=failed_records,
|
|
errors=errors[:10], # Limit errors to first 10
|
|
duration_seconds=round(duration, 2)
|
|
)
|
|
|
|
except Exception as e:
|
|
return MigrationResponse(
|
|
status="failed",
|
|
total_records=total_records,
|
|
migrated_records=migrated_records,
|
|
failed_records=failed_records,
|
|
errors=[{"error": str(e)}],
|
|
duration_seconds=round(time.time() - start_time, 2)
|
|
)
|
|
|
|
@router.get(
|
|
"/migration/export/{ticker}",
|
|
summary="Export data for migration",
|
|
description="Export financial data for a specific ticker (used by migration process)"
|
|
)
|
|
async def export_data(
|
|
ticker: str,
|
|
start_date: Optional[datetime] = None,
|
|
end_date: Optional[datetime] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: None = Depends(verify_migration_key)
|
|
):
|
|
"""Export data for migration"""
|
|
|
|
# This endpoint would be used by the migration process
|
|
# Implementation depends on specific needs
|
|
|
|
return {
|
|
"ticker": ticker,
|
|
"message": "Export endpoint for migration",
|
|
"note": "This would return raw data for migration purposes"
|
|
} |