629 lines
23 KiB
Python
629 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
FastAPI DeQuienes Provider
|
|
"""
|
|
|
|
from multiprocessing import process
|
|
import os
|
|
import json
|
|
import time
|
|
import asyncio
|
|
import logging
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
from datetime import datetime, timedelta
|
|
|
|
import requests
|
|
from fastapi import FastAPI, HTTPException, Depends, status, Header
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel, Field, validator
|
|
from dotenv import load_dotenv
|
|
from pymongo import MongoClient
|
|
from pymongo.errors import ConnectionFailure
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# MongoDB connection setup
|
|
MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/duxiter")
|
|
CACHE_TTL_SECONDS = int(os.getenv("DEQUIENES_CACHE_TTL_SECONDS", "604800")) # Default 1 week
|
|
|
|
try:
|
|
mongo_client = MongoClient(MONGODB_URI)
|
|
# Test connection
|
|
mongo_client.admin.command('ping')
|
|
db = mongo_client.get_database()
|
|
cache_collection = db.dequienes_cache
|
|
|
|
# Create TTL index for automatic document expiration
|
|
cache_collection.create_index("expires_at", expireAfterSeconds=0)
|
|
logger.info(f"MongoDB connected successfully. Cache TTL: {CACHE_TTL_SECONDS} seconds")
|
|
except ConnectionFailure as e:
|
|
logger.error(f"MongoDB connection failed: {e}")
|
|
mongo_client = None
|
|
db = None
|
|
cache_collection = None
|
|
|
|
# Constants
|
|
|
|
DEQUIENES_BASE_URL = "https://dequienes.cl/api/graph/relationships"
|
|
DEQUIENES_RESPONSES_DIR = Path(__file__).resolve().parents[2] / "dequenes_responses"
|
|
|
|
# Pydantic Models
|
|
class PersonalInformation(BaseModel):
|
|
chileanRut: str = Field(..., description="Chilean RUT number")
|
|
# chileanSerialNumber: Optional[str] = Field(None, description="Chilean serial number (not required for empresarial)")
|
|
|
|
@validator('chileanRut')
|
|
def validate_rut(cls, v):
|
|
if not v or not v.strip():
|
|
raise ValueError('Chilean RUT is required')
|
|
return v.strip()
|
|
|
|
class PrimaryConsumer(BaseModel):
|
|
personalInformation: PersonalInformation
|
|
|
|
class Applicants(BaseModel):
|
|
primaryConsumer: PrimaryConsumer
|
|
|
|
class ProductData(BaseModel):
|
|
billTo: str = Field(..., description="Bill to information")
|
|
shipTo: str = Field(..., description="Ship to information")
|
|
productName: str = Field(..., description="Product name")
|
|
productOrch: str = Field(..., description="Product orchestration")
|
|
configuration: str = Field(default="Config", description="Configuration")
|
|
customer: str = Field(..., description="Customer information")
|
|
model: str = Field(..., description="Model information")
|
|
|
|
@validator('*', pre=True)
|
|
def strip_strings(cls, v):
|
|
if isinstance(v, str):
|
|
return v.strip()
|
|
return v
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class RelationshipQueryParams(BaseModel):
|
|
distance: int = Field(default=2)
|
|
relationship_direction: str = Field(default="BOTH")
|
|
one_path_per_node: bool = Field(default=False)
|
|
|
|
class TokenResponse(BaseModel):
|
|
pass
|
|
|
|
class HealthResponse(BaseModel):
|
|
status: str
|
|
timestamp: str
|
|
version: str
|
|
|
|
class ErrorResponse(BaseModel):
|
|
error: str
|
|
detail: str
|
|
timestamp: str
|
|
|
|
# FastAPI App
|
|
app = FastAPI(
|
|
title="DeQuienes Provider API",
|
|
description="REST API per integrazione DeQuienes relationships",
|
|
version="1.0.0",
|
|
docs_url="/docs",
|
|
redoc_url="/redoc"
|
|
)
|
|
|
|
# CORS middleware
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # Configure appropriately for production
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Cache helper functions
|
|
def generate_cache_key(env_key: str, payload: Dict[str, Any]) -> str:
|
|
"""Generate a unique cache key based on environment and payload"""
|
|
# Create a normalized string representation of the payload
|
|
payload_str = json.dumps(payload, sort_keys=True, separators=(',', ':'))
|
|
# Create hash of the payload for consistent key generation
|
|
payload_hash = hashlib.sha256(payload_str.encode()).hexdigest()
|
|
return f"dequienes_cache:{env_key}:{payload_hash}"
|
|
|
|
def get_cached_transaction(cache_key: str) -> Optional[Dict[str, Any]]:
|
|
|
|
"""Retrieve cached transaction result if available and not expired"""
|
|
if cache_collection is None:
|
|
return None
|
|
|
|
try:
|
|
cached_doc = cache_collection.find_one({"_id": cache_key})
|
|
if cached_doc:
|
|
logger.info(f"Cache hit for key: {cache_key}")
|
|
return cached_doc.get("result")
|
|
else:
|
|
logger.info(f"Cache miss for key: {cache_key}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error retrieving from cache: {e}")
|
|
return None
|
|
|
|
def save_transaction_to_cache(cache_key: str, result: Dict[str, Any]) -> None:
|
|
"""Save transaction result to cache with TTL"""
|
|
if cache_collection is None:
|
|
return
|
|
|
|
try:
|
|
expires_at = datetime.utcnow() + timedelta(seconds=CACHE_TTL_SECONDS)
|
|
cache_doc = {
|
|
"_id": cache_key,
|
|
"result": result,
|
|
"cached_at": datetime.utcnow(),
|
|
"expires_at": expires_at
|
|
}
|
|
|
|
# Use upsert to replace existing cache entry
|
|
cache_collection.replace_one(
|
|
{"_id": cache_key},
|
|
cache_doc,
|
|
upsert=True
|
|
)
|
|
logger.info(f"Transaction result cached with key: {cache_key}, expires at: {expires_at}")
|
|
except Exception as e:
|
|
logger.error(f"Error saving to cache: {e}")
|
|
|
|
def clear_transaction_cache() -> int:
|
|
"""Clear all cached transactions and return count of deleted documents"""
|
|
if cache_collection is None:
|
|
return 0
|
|
|
|
try:
|
|
result = cache_collection.delete_many({})
|
|
logger.info(f"Cleared {result.deleted_count} cached transactions")
|
|
return result.deleted_count
|
|
except Exception as e:
|
|
logger.error(f"Error clearing cache: {e}")
|
|
return 0
|
|
|
|
def get_cache_stats() -> Dict[str, Any]:
|
|
"""Get cache statistics"""
|
|
if cache_collection is None:
|
|
return {"error": "Cache not available"}
|
|
|
|
try:
|
|
total_docs = cache_collection.count_documents({})
|
|
expired_docs = cache_collection.count_documents({
|
|
"expires_at": {"$lt": datetime.utcnow()}
|
|
})
|
|
|
|
return {
|
|
"total_cached_transactions": total_docs,
|
|
"expired_transactions": expired_docs,
|
|
"active_transactions": total_docs - expired_docs,
|
|
"cache_ttl_seconds": CACHE_TTL_SECONDS,
|
|
"mongodb_connected": True
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting cache stats: {e}")
|
|
return {"error": str(e), "mongodb_connected": False}
|
|
|
|
def now() -> float:
|
|
return time.time()
|
|
|
|
def execute_get(url: str, headers: Dict[str, str], cache_key: str) -> Dict[str, Any]:
|
|
cached_result = get_cached_transaction(cache_key)
|
|
if cached_result:
|
|
cached_result["_cache_info"] = {"cached": True, "cache_key": cache_key, "retrieved_at": datetime.utcnow().isoformat()}
|
|
return cached_result
|
|
resp = requests.get(url, headers=headers, timeout=60)
|
|
if resp.status_code == 200:
|
|
try:
|
|
data = resp.json()
|
|
except Exception:
|
|
data = {"raw": resp.text}
|
|
save_transaction_to_cache(cache_key, data)
|
|
data["_cache_info"] = {"cached": False, "cache_key": cache_key, "cached_at": datetime.utcnow().isoformat()}
|
|
return data
|
|
if resp.status_code == 401:
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
if resp.status_code == 403:
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
if resp.status_code == 404:
|
|
raise HTTPException(status_code=404, detail="Not Found")
|
|
if resp.status_code >= 500:
|
|
raise HTTPException(status_code=502, detail=f"Upstream error {resp.status_code}")
|
|
raise HTTPException(status_code=resp.status_code, detail=resp.text)
|
|
|
|
def persist_response(rut_sanitized: str, payload: Dict[str, Any], prefix: str = "relationships") -> str:
|
|
try:
|
|
DEQUIENES_RESPONSES_DIR.mkdir(parents=True, exist_ok=True)
|
|
file_path = DEQUIENES_RESPONSES_DIR / f"{prefix}_{rut_sanitized}_{int(time.time())}.json"
|
|
with file_path.open("w", encoding="utf-8") as f:
|
|
json.dump(payload, f, ensure_ascii=False, indent=2)
|
|
return str(file_path)
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
# Rimuoviamo completamente flusso POST/Equifax
|
|
|
|
|
|
def _clear_cached_token(env_key: str) -> None:
|
|
"""Clear cached token for the given environment to force refresh"""
|
|
try:
|
|
cache_data = read_cache()
|
|
if cache_data and env_key in cache_data:
|
|
del cache_data[env_key]
|
|
write_cache(cache_data)
|
|
logger.info(f"Cleared cached token for environment: {env_key}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to clear cached token: {e}")
|
|
|
|
pass
|
|
|
|
# API Endpoints
|
|
@app.get("/", response_model=HealthResponse)
|
|
async def root():
|
|
"""Health check endpoint"""
|
|
return HealthResponse(
|
|
status="healthy",
|
|
timestamp=datetime.now().isoformat(),
|
|
version="1.0.0"
|
|
)
|
|
|
|
@app.get("/health", response_model=HealthResponse)
|
|
async def health_check():
|
|
"""Detailed health check"""
|
|
return HealthResponse(
|
|
status="healthy",
|
|
timestamp=datetime.now().isoformat(),
|
|
version="1.0.0"
|
|
)
|
|
|
|
@app.get("/relationships/{rut}")
|
|
async def get_relationships(rut: str, distance: int = 2, relationship_direction: str = "BOTH", one_path_per_node: bool = False, x_api_key: Optional[str] = Header(default=None, alias="x-api-key")):
|
|
if relationship_direction not in ("BOTH", "OUTGOING", "INCOMING"):
|
|
raise HTTPException(status_code=400, detail="Invalid relationship_direction")
|
|
api_key = x_api_key or os.getenv("DEQUIENES_API_KEY", "").strip()
|
|
if not api_key:
|
|
raise HTTPException(status_code=500, detail="Missing DeQuienes API key")
|
|
rut_sanitized = str(rut).split('-')[0].strip()
|
|
url = f"{DEQUIENES_BASE_URL}/{rut_sanitized}?distance={distance}&relationship_direction={relationship_direction}&one_path_per_node={'true' if one_path_per_node else 'false'}"
|
|
cache_key = generate_cache_key("dequienes_relationships", {
|
|
"rut": rut_sanitized,
|
|
"distance": int(distance),
|
|
"relationship_direction": relationship_direction,
|
|
"one_path_per_node": bool(one_path_per_node)
|
|
})
|
|
cached = get_cached_transaction(cache_key)
|
|
if cached:
|
|
cached["_cache_info"] = {
|
|
"cached": True,
|
|
"cache_key": cache_key,
|
|
"retrieved_at": datetime.utcnow().isoformat()
|
|
}
|
|
persist_response(rut_sanitized, cached)
|
|
return cached
|
|
headers = {"accept": "application/json", "x-api-key": api_key}
|
|
try:
|
|
resp = requests.get(url, headers=headers, timeout=60)
|
|
if resp.status_code == 200:
|
|
try:
|
|
data = resp.json()
|
|
except Exception:
|
|
data = {"raw": resp.text}
|
|
save_transaction_to_cache(cache_key, data)
|
|
data["_cache_info"] = {
|
|
"cached": False,
|
|
"cache_key": cache_key,
|
|
"cached_at": datetime.utcnow().isoformat()
|
|
}
|
|
persist_response(rut_sanitized, data)
|
|
return data
|
|
if resp.status_code == 401:
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
if resp.status_code == 403:
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
if resp.status_code == 404:
|
|
raise HTTPException(status_code=404, detail="Not Found")
|
|
if resp.status_code >= 500:
|
|
raise HTTPException(status_code=502, detail=f"Upstream error {resp.status_code}")
|
|
raise HTTPException(status_code=resp.status_code, detail=resp.text)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
class LegalEvent(BaseModel):
|
|
cve: Optional[str] = None
|
|
published_at: Optional[str] = None
|
|
actuation_type: str
|
|
company_name: Optional[str] = None
|
|
source: str
|
|
url: Optional[str] = None
|
|
|
|
class LegalEventsResponse(BaseModel):
|
|
rut: str
|
|
count: int
|
|
cached: bool
|
|
timestamp: str
|
|
events: list[LegalEvent]
|
|
constitution_date: Optional[str] = None
|
|
|
|
def sanitize_rut_without_dv(rut: str) -> str:
|
|
v = (rut or "").upper().replace(".", "").replace("-", "").strip()
|
|
if len(v) > 8:
|
|
return v[:-1]
|
|
return v
|
|
|
|
def map_actuation_type(src: str, info: Dict[str, Any], label: Optional[str]) -> str:
|
|
s = (src or "").upper()
|
|
if s in ("DO", "DOH"):
|
|
mt = str(info.get("match_type", "")).lower()
|
|
if "tt" in mt:
|
|
return "constitution"
|
|
return "actuation"
|
|
if s == "RES":
|
|
return "actuation"
|
|
return label or "actuation"
|
|
|
|
@app.get("/api/legal-events/{rut}", response_model=LegalEventsResponse)
|
|
async def get_legal_events(rut: str, x_api_key: Optional[str] = Header(default=None, alias="x-api-key")):
|
|
logger.info(f"Requesting legal events for RUT: {rut}")
|
|
api_key = x_api_key or os.getenv("DEQUIENES_API_KEY", "").strip()
|
|
if not api_key:
|
|
raise HTTPException(status_code=500, detail="Missing DeQuienes API key")
|
|
|
|
rut_sanitized = sanitize_rut_without_dv(rut)
|
|
# url = f"{DEQUIENES_BASE_URL}/{rut_sanitized}?distance=1&relationship_direction=BOTH&one_path_per_node=false"
|
|
url = f"https://dequienes.cl/api/legal-events/{rut_sanitized}"
|
|
logger.info(f"legal events Requesting URL: {url}")
|
|
cache_key = generate_cache_key("dequienes_legal_events", {"rut": rut_sanitized})
|
|
use_cache = True
|
|
# Try to serve from cache first
|
|
if use_cache:
|
|
cached_doc = get_cached_transaction(cache_key)
|
|
if cached_doc and isinstance(cached_doc, dict) and "events" in cached_doc:
|
|
persisted_payload = {
|
|
"rut": rut_sanitized,
|
|
"events": cached_doc.get("events", []),
|
|
"cached": True,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"constitution_date": cached_doc.get("constitution_date")
|
|
}
|
|
persist_response(rut_sanitized, persisted_payload, prefix="legal_events")
|
|
return LegalEventsResponse(
|
|
rut=rut_sanitized,
|
|
count=len(cached_doc.get("events", [])),
|
|
cached=True,
|
|
timestamp=datetime.utcnow().isoformat(),
|
|
events=[LegalEvent(**e) for e in cached_doc.get("events", [])],
|
|
constitution_date=cached_doc.get("constitution_date")
|
|
)
|
|
|
|
headers = {"accept": "application/json", "x-api-key": api_key}
|
|
logger.info(f"Requesting URL: {url}")
|
|
try:
|
|
resp = requests.get(url, headers=headers, timeout=60)
|
|
logger.info(json.dumps(resp.json(), indent=2))
|
|
|
|
if resp.status_code != 200:
|
|
if resp.status_code == 401:
|
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
if resp.status_code == 403:
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
if resp.status_code == 404:
|
|
raise HTTPException(status_code=404, detail="Not Found")
|
|
if resp.status_code >= 500:
|
|
raise HTTPException(status_code=502, detail=f"Upstream error {resp.status_code}")
|
|
raise HTTPException(status_code=resp.status_code, detail=resp.text)
|
|
|
|
try:
|
|
data = resp.json()
|
|
except Exception:
|
|
raise HTTPException(status_code=502, detail="Invalid JSON from upstream")
|
|
|
|
legal_events = data.get("legal_events", [])
|
|
events: list[Dict[str, Any]] = []
|
|
for ev in legal_events:
|
|
e = {
|
|
"cve": ev.get("cve"),
|
|
"published_at": ev.get("published_at"),
|
|
"actuation_type": ev.get("actuation_type"),
|
|
"company_name": ev.get("company_name"),
|
|
"source": ev.get("source"),
|
|
"url": ev.get("url")
|
|
}
|
|
events.append(e)
|
|
|
|
constitution_candidates = [
|
|
ev for ev in events
|
|
if str(ev.get("actuation_type", "")).upper().startswith("CONSTITU")
|
|
]
|
|
constitution_date_val = None
|
|
if constitution_candidates:
|
|
def _parse_date(s: Optional[str]) -> Optional[datetime]:
|
|
try:
|
|
return datetime.fromisoformat(str(s))
|
|
except Exception:
|
|
return None
|
|
dates = [(_parse_date(ev.get("published_at")), ev.get("published_at")) for ev in constitution_candidates]
|
|
dates = [d for d in dates if d[0] is not None]
|
|
if dates:
|
|
dates.sort(key=lambda x: x[0])
|
|
constitution_date_val = dates[0][1]
|
|
|
|
result_doc = {"events": events, "constitution_date": constitution_date_val}
|
|
save_transaction_to_cache(cache_key, result_doc)
|
|
persisted_payload = {
|
|
"rut": rut_sanitized,
|
|
"events": events,
|
|
"cached": False,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"constitution_date": constitution_date_val
|
|
}
|
|
persist_response(rut_sanitized, persisted_payload, prefix="legal_events")
|
|
|
|
return LegalEventsResponse(
|
|
rut=rut_sanitized,
|
|
count=len(events),
|
|
cached=False,
|
|
timestamp=datetime.utcnow().isoformat(),
|
|
events=[LegalEvent(**e) for e in events],
|
|
constitution_date=constitution_date_val
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.delete("/relationships-cache")
|
|
async def clear_relationships_cache():
|
|
try:
|
|
deleted_count = clear_transaction_cache()
|
|
return {"message": "Cache cleared successfully", "deleted_count": deleted_count}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to clear cache: {str(e)}")
|
|
|
|
@app.get("/relationships-cache/stats")
|
|
async def get_relationships_cache_stats():
|
|
try:
|
|
stats = get_cache_stats()
|
|
return stats
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get cache stats: {str(e)}")
|
|
|
|
# Rimuoviamo endpoints legacy transaction-cache
|
|
|
|
@app.get("/config")
|
|
async def get_environment_config():
|
|
try:
|
|
api_key = os.getenv("DEQUIENES_API_KEY", "")
|
|
return {"api_key_set": bool(api_key), "cache_ttl_seconds": CACHE_TTL_SECONDS}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get config: {str(e)}")
|
|
|
|
# Error handlers
|
|
@app.exception_handler(HTTPException)
|
|
async def http_exception_handler(request, exc):
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content=ErrorResponse(
|
|
error=exc.detail,
|
|
detail=str(exc.detail),
|
|
timestamp=datetime.now().isoformat()
|
|
).model_dump()
|
|
)
|
|
|
|
@app.exception_handler(Exception)
|
|
async def general_exception_handler(request, exc):
|
|
logger.error(f"Unhandled exception: {exc}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content=ErrorResponse(
|
|
error="Internal server error",
|
|
detail=str(exc),
|
|
timestamp=datetime.now().isoformat()
|
|
).model_dump()
|
|
)
|
|
|
|
async def cleanup_old_responses():
|
|
"""
|
|
Periodically cleans up old response files:
|
|
1. Deletes files older than 1 week.
|
|
2. For remaining files, keeps only the most recent one for each (prefix, rut) pair.
|
|
Runs every 10 minutes.
|
|
"""
|
|
while True:
|
|
try:
|
|
logger.info("Starting cleanup of old responses...")
|
|
if not DEQUIENES_RESPONSES_DIR.exists():
|
|
logger.info(f"Directory {DEQUIENES_RESPONSES_DIR} does not exist. Skipping cleanup.")
|
|
else:
|
|
files_by_group: Dict[str, list] = {}
|
|
one_week_ago = time.time() - (7 * 24 * 60 * 60)
|
|
deleted_count = 0
|
|
|
|
# Group files and filter old ones
|
|
for file_path in DEQUIENES_RESPONSES_DIR.glob("*.json"):
|
|
try:
|
|
filename = file_path.stem
|
|
# Expecting format: {prefix}_{rut}_{timestamp}
|
|
# We split by '_' from the right once to get the timestamp
|
|
parts = filename.rsplit('_', 1)
|
|
if len(parts) != 2:
|
|
continue
|
|
|
|
base_name, timestamp_str = parts
|
|
|
|
# Verify timestamp is integer
|
|
if not timestamp_str.isdigit():
|
|
continue
|
|
|
|
timestamp = int(timestamp_str)
|
|
|
|
# Check if older than 1 week
|
|
if timestamp < one_week_ago:
|
|
try:
|
|
file_path.unlink()
|
|
deleted_count += 1
|
|
logger.info(f"Deleted old response file (older than 1 week): {file_path.name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete {file_path}: {e}")
|
|
continue
|
|
|
|
if base_name not in files_by_group:
|
|
files_by_group[base_name] = []
|
|
|
|
files_by_group[base_name].append((timestamp, file_path))
|
|
except Exception as e:
|
|
logger.warning(f"Error processing file {file_path}: {e}")
|
|
|
|
# Process groups
|
|
for base_name, file_list in files_by_group.items():
|
|
# Sort by timestamp descending (newest first)
|
|
file_list.sort(key=lambda x: x[0], reverse=True)
|
|
|
|
# Keep the first one, delete the rest
|
|
for _, file_to_delete in file_list[1:]:
|
|
try:
|
|
file_to_delete.unlink()
|
|
deleted_count += 1
|
|
logger.info(f"Deleted old response file (duplicate): {file_to_delete.name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete {file_to_delete}: {e}")
|
|
|
|
if deleted_count > 0:
|
|
logger.info(f"Cleanup finished. Deleted {deleted_count} old files.")
|
|
else:
|
|
logger.info("Cleanup finished. No files to delete.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during cleanup: {e}")
|
|
|
|
# Wait for 10 minutes (600 seconds)
|
|
await asyncio.sleep(600)
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
asyncio.create_task(cleanup_old_responses())
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
#print(os.environ)
|
|
#uvicorn.run(app, host="0.0.0.0", port=8023)
|
|
logger.info("Starting server on port 8023")
|
|
uvicorn.run(app, port=8023)
|