Spaces:
Sleeping
Sleeping
Commit
·
d1bf1d0
0
Parent(s):
initial commit
Browse files- .DS_Store +0 -0
- .env.example +30 -0
- .gitignore +2 -0
- README.md +38 -0
- app/__init__.py +6 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/__pycache__/main.cpython-313.pyc +0 -0
- app/api/__pycache__/dependencies.cpython-313.pyc +0 -0
- app/api/dependencies.py +56 -0
- app/api/routes/__init__.py +7 -0
- app/api/routes/__pycache__/__init__.cpython-313.pyc +0 -0
- app/api/routes/__pycache__/health.cpython-313.pyc +0 -0
- app/api/routes/__pycache__/task.cpython-313.pyc +0 -0
- app/api/routes/health.py +42 -0
- app/api/routes/task.py +64 -0
- app/core/__pycache__/config.cpython-313.pyc +0 -0
- app/core/__pycache__/exceptions.cpython-313.pyc +0 -0
- app/core/__pycache__/logging.cpython-313.pyc +0 -0
- app/core/__pycache__/security.cpython-313.pyc +0 -0
- app/core/config.py +74 -0
- app/core/exceptions.py +144 -0
- app/core/logging.py +65 -0
- app/core/security.py +57 -0
- app/main.py +99 -0
- app/middleware/__pycache__/logging.cpython-313.pyc +0 -0
- app/middleware/logging.py +51 -0
- app/models/__pycache__/request.cpython-313.pyc +0 -0
- app/models/__pycache__/response.cpython-313.pyc +0 -0
- app/models/request.py +58 -0
- app/models/response.py +74 -0
- app/services/__pycache__/task_processor.cpython-313.pyc +0 -0
- app/services/task_processor.py +56 -0
- dockerfile +22 -0
- requirements.txt +18 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.env.example
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Application Configuration
|
| 2 |
+
APP_NAME=LLM Analysis Quiz API
|
| 3 |
+
ENVIRONMENT=development
|
| 4 |
+
|
| 5 |
+
# Server Configuration
|
| 6 |
+
HOST=0.0.0.0
|
| 7 |
+
PORT=8000
|
| 8 |
+
|
| 9 |
+
# Security
|
| 10 |
+
API_SECRET=your-secret-key-here
|
| 11 |
+
ALLOWED_ORIGINS=*
|
| 12 |
+
|
| 13 |
+
# Logging
|
| 14 |
+
LOG_LEVEL=INFO
|
| 15 |
+
LOG_DIR=logs
|
| 16 |
+
|
| 17 |
+
# Task Processing
|
| 18 |
+
TASK_TIMEOUT=300
|
| 19 |
+
MAX_RETRIES=3
|
| 20 |
+
|
| 21 |
+
# External APIs
|
| 22 |
+
OPENAI_API_KEY=
|
| 23 |
+
ANTHROPIC_API_KEY=
|
| 24 |
+
|
| 25 |
+
# Database (optional)
|
| 26 |
+
DATABASE_URL=
|
| 27 |
+
|
| 28 |
+
# Redis (optional)
|
| 29 |
+
REDIS_URL=
|
| 30 |
+
LOGTAIL_SOURCE_TOKEN=
|
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
logs
|
README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: LLM QUIZ ANALYSIS API
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
sdk_version: "0.0.1"
|
| 8 |
+
app_file: app/main.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
Check out the configuration reference at [https://huggingface.co/docs/hub/spaces-config-reference](https://huggingface.co/docs/hub/spaces-config-reference)
|
| 13 |
+
|
| 14 |
+
## LLM Code Deployment API
|
| 15 |
+
|
| 16 |
+
This Space provides a production-grade FastAPI backend for automated application generation and deployment, as described in the LLM Code Deployment project. It supports secure secret-based access, LLM-driven app generation, automated deployment to GitHub Pages, and evaluation callbacks—built with best practices for industry and Hugging Face Spaces Docker deployments.
|
| 17 |
+
|
| 18 |
+
### Key Features
|
| 19 |
+
|
| 20 |
+
- **POST `/handle-task`**: Receives and processes app brief requests, verifies secrets, and triggers app generation workflows.
|
| 21 |
+
- **Dockerized Deployment**: Secure, reproducible builds using a custom Dockerfile following Hugging Face recommendations.
|
| 22 |
+
- **Secrets Management**: Reads secrets via Hugging Face Spaces environment for maximum security (never hardcoded).
|
| 23 |
+
- **GitHub Automation**: Automatically creates public repos, populates README and LICENSE, and enables GitHub Pages.
|
| 24 |
+
|
| 25 |
+
### Usage
|
| 26 |
+
|
| 27 |
+
1. Set up required Space secrets (secrets and tokens via the Spaces UI).
|
| 28 |
+
2. Deploy or push your code to this Space.
|
| 29 |
+
3. POST a task request (see `/handle-task` endpoint documentation).
|
| 30 |
+
|
| 31 |
+
### Development
|
| 32 |
+
|
| 33 |
+
See `requirements.txt` for dependencies.
|
| 34 |
+
Your FastAPI entry point should be specified in `app/main.py`.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
For further configuration options, please visit the [Spaces Configuration Reference](https://huggingface.co/docs/hub/spaces-config-reference).
|
app/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM Analysis Quiz API Application Package
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
__version__ = "1.0.0"
|
| 6 |
+
__author__ = "Your Name"
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (300 Bytes). View file
|
|
|
app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (3.86 kB). View file
|
|
|
app/api/__pycache__/dependencies.cpython-313.pyc
ADDED
|
Binary file (2.08 kB). View file
|
|
|
app/api/dependencies.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API Dependencies
|
| 3 |
+
Reusable dependency injection functions
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import Request, HTTPException, status
|
| 7 |
+
|
| 8 |
+
from app.core.security import verify_secret
|
| 9 |
+
from app.core.exceptions import AuthenticationError
|
| 10 |
+
from app.core.logging import get_logger
|
| 11 |
+
|
| 12 |
+
logger = get_logger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
async def get_request_body(request: Request) -> dict:
|
| 16 |
+
"""
|
| 17 |
+
Extract and validate JSON body from request
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
request: FastAPI Request object
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
dict: Parsed JSON body
|
| 24 |
+
|
| 25 |
+
Raises:
|
| 26 |
+
HTTPException: If JSON parsing fails
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
body = await request.json()
|
| 30 |
+
logger.debug(f"Request body received: {list(body.keys())}")
|
| 31 |
+
return body
|
| 32 |
+
except Exception as e:
|
| 33 |
+
logger.error(f"Failed to parse JSON: {str(e)}")
|
| 34 |
+
raise HTTPException(
|
| 35 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 36 |
+
detail="Invalid JSON format in request body"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def verify_authentication(secret: str) -> bool:
|
| 41 |
+
"""
|
| 42 |
+
Verify request authentication
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
secret: Secret from request
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
bool: True if authenticated
|
| 49 |
+
|
| 50 |
+
Raises:
|
| 51 |
+
AuthenticationError: If authentication fails
|
| 52 |
+
"""
|
| 53 |
+
if not verify_secret(secret):
|
| 54 |
+
raise AuthenticationError("Invalid secret. Authentication failed.")
|
| 55 |
+
|
| 56 |
+
return True
|
app/api/routes/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API Routes Package
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from app.api.routes import task, health
|
| 6 |
+
|
| 7 |
+
__all__ = ["task", "health"]
|
app/api/routes/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (330 Bytes). View file
|
|
|
app/api/routes/__pycache__/health.cpython-313.pyc
ADDED
|
Binary file (1.63 kB). View file
|
|
|
app/api/routes/__pycache__/task.cpython-313.pyc
ADDED
|
Binary file (2.82 kB). View file
|
|
|
app/api/routes/health.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Health Check Routes
|
| 3 |
+
Endpoints for monitoring application health
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter
|
| 7 |
+
|
| 8 |
+
from app.core.config import settings
|
| 9 |
+
from app.core.logging import get_logger
|
| 10 |
+
from app.models.response import HealthResponse
|
| 11 |
+
|
| 12 |
+
logger = get_logger(__name__)
|
| 13 |
+
|
| 14 |
+
router = APIRouter()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@router.get("/", response_model=dict)
|
| 18 |
+
async def root():
|
| 19 |
+
"""
|
| 20 |
+
Root endpoint - basic health check
|
| 21 |
+
"""
|
| 22 |
+
logger.debug("Root endpoint accessed")
|
| 23 |
+
return {
|
| 24 |
+
"status": "online",
|
| 25 |
+
"service": settings.APP_NAME,
|
| 26 |
+
"version": settings.APP_VERSION
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@router.get("/health", response_model=HealthResponse)
|
| 31 |
+
async def health_check():
|
| 32 |
+
"""
|
| 33 |
+
Detailed health check endpoint
|
| 34 |
+
"""
|
| 35 |
+
logger.debug("Health check requested")
|
| 36 |
+
|
| 37 |
+
return HealthResponse(
|
| 38 |
+
status="healthy",
|
| 39 |
+
environment=settings.ENVIRONMENT,
|
| 40 |
+
version=settings.APP_VERSION,
|
| 41 |
+
secret_configured=settings.is_secret_configured()
|
| 42 |
+
)
|
app/api/routes/task.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task Processing Routes
|
| 3 |
+
Main API endpoint for handling task requests
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from fastapi import APIRouter, Request, status
|
| 8 |
+
|
| 9 |
+
from app.models.request import TaskRequest
|
| 10 |
+
from app.models.response import TaskResponse
|
| 11 |
+
from app.api.dependencies import verify_authentication
|
| 12 |
+
from app.services.task_processor import TaskProcessor
|
| 13 |
+
from app.core.logging import get_logger
|
| 14 |
+
|
| 15 |
+
logger = get_logger(__name__)
|
| 16 |
+
|
| 17 |
+
router = APIRouter()
|
| 18 |
+
task_processor = TaskProcessor()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@router.post("/api/", response_model=TaskResponse, status_code=status.HTTP_200_OK)
|
| 22 |
+
async def handle_task(request: Request):
|
| 23 |
+
"""
|
| 24 |
+
Main API endpoint for handling task requests
|
| 25 |
+
|
| 26 |
+
- Validates request format (HTTP 400 if invalid)
|
| 27 |
+
- Verifies secret (HTTP 403 if invalid)
|
| 28 |
+
- Processes task and returns results (HTTP 200 if successful)
|
| 29 |
+
"""
|
| 30 |
+
start_time = datetime.now()
|
| 31 |
+
|
| 32 |
+
logger.info("📥 Task request received")
|
| 33 |
+
|
| 34 |
+
# Parse and validate request body with Pydantic
|
| 35 |
+
body = await request.json()
|
| 36 |
+
task_data = TaskRequest(**body)
|
| 37 |
+
|
| 38 |
+
logger.info(f"✅ Request validated for: {task_data.email}")
|
| 39 |
+
|
| 40 |
+
# Verify authentication
|
| 41 |
+
logger.info("🔐 Verifying authentication")
|
| 42 |
+
verify_authentication(task_data.secret)
|
| 43 |
+
logger.info("✅ Authentication successful")
|
| 44 |
+
|
| 45 |
+
# Process the task
|
| 46 |
+
logger.info("🚀 Starting task execution")
|
| 47 |
+
result_data = await task_processor.process(task_data)
|
| 48 |
+
|
| 49 |
+
# Calculate execution time
|
| 50 |
+
execution_time = (datetime.now() - start_time).total_seconds()
|
| 51 |
+
logger.info(f"⏱️ Task completed in {execution_time:.3f}s")
|
| 52 |
+
|
| 53 |
+
# Prepare response
|
| 54 |
+
response = TaskResponse(
|
| 55 |
+
success=True,
|
| 56 |
+
message="Task completed successfully",
|
| 57 |
+
data=result_data,
|
| 58 |
+
email=task_data.email,
|
| 59 |
+
task_url=str(task_data.url),
|
| 60 |
+
execution_time=execution_time
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
logger.info("✅ Response prepared successfully")
|
| 64 |
+
return response
|
app/core/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (3.13 kB). View file
|
|
|
app/core/__pycache__/exceptions.cpython-313.pyc
ADDED
|
Binary file (6.21 kB). View file
|
|
|
app/core/__pycache__/logging.cpython-313.pyc
ADDED
|
Binary file (3.37 kB). View file
|
|
|
app/core/__pycache__/security.cpython-313.pyc
ADDED
|
Binary file (2.06 kB). View file
|
|
|
app/core/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Application Configuration Management
|
| 3 |
+
Centralizes all environment variables and settings
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from typing import List
|
| 8 |
+
from pydantic_settings import BaseSettings
|
| 9 |
+
from pydantic import Field
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Settings(BaseSettings):
|
| 13 |
+
"""
|
| 14 |
+
Application settings loaded from environment variables
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
# Application
|
| 18 |
+
APP_NAME: str = "LLM Analysis Quiz API"
|
| 19 |
+
APP_DESCRIPTION: str = "API endpoint for handling dynamic data analysis tasks"
|
| 20 |
+
APP_VERSION: str = "1.0.0"
|
| 21 |
+
ENVIRONMENT: str = Field(default="development", env="ENVIRONMENT")
|
| 22 |
+
|
| 23 |
+
# Server
|
| 24 |
+
HOST: str = Field(default="0.0.0.0", env="HOST")
|
| 25 |
+
PORT: int = Field(default=8000, env="PORT")
|
| 26 |
+
|
| 27 |
+
# Security
|
| 28 |
+
API_SECRET: str = Field(default="", env="API_SECRET")
|
| 29 |
+
ALLOWED_ORIGINS: List[str] = Field(
|
| 30 |
+
default=["*"],
|
| 31 |
+
env="ALLOWED_ORIGINS"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Logging
|
| 35 |
+
LOG_LEVEL: str = Field(default="INFO", env="LOG_LEVEL")
|
| 36 |
+
LOG_DIR: str = Field(default="logs", env="LOG_DIR")
|
| 37 |
+
# Cloud Logging
|
| 38 |
+
LOGTAIL_SOURCE_TOKEN: str = Field(default="", env="LOGTAIL_SOURCE_TOKEN")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# Task Processing
|
| 42 |
+
TASK_TIMEOUT: int = Field(default=300, env="TASK_TIMEOUT") # 5 minutes
|
| 43 |
+
MAX_RETRIES: int = Field(default=3, env="MAX_RETRIES")
|
| 44 |
+
|
| 45 |
+
# External APIs (to be added as needed)
|
| 46 |
+
OPENAI_API_KEY: str = Field(default="", env="OPENAI_API_KEY")
|
| 47 |
+
ANTHROPIC_API_KEY: str = Field(default="", env="ANTHROPIC_API_KEY")
|
| 48 |
+
|
| 49 |
+
# Database (for future use)
|
| 50 |
+
DATABASE_URL: str = Field(default="", env="DATABASE_URL")
|
| 51 |
+
|
| 52 |
+
# Redis (for caching, future use)
|
| 53 |
+
REDIS_URL: str = Field(default="", env="REDIS_URL")
|
| 54 |
+
|
| 55 |
+
class Config:
|
| 56 |
+
env_file = ".env"
|
| 57 |
+
env_file_encoding = "utf-8"
|
| 58 |
+
case_sensitive = True
|
| 59 |
+
|
| 60 |
+
def is_secret_configured(self) -> bool:
|
| 61 |
+
"""Check if API secret is configured"""
|
| 62 |
+
return bool(self.API_SECRET and self.API_SECRET.strip())
|
| 63 |
+
|
| 64 |
+
def is_production(self) -> bool:
|
| 65 |
+
"""Check if running in production"""
|
| 66 |
+
return self.ENVIRONMENT.lower() == "production"
|
| 67 |
+
|
| 68 |
+
def is_development(self) -> bool:
|
| 69 |
+
"""Check if running in development"""
|
| 70 |
+
return self.ENVIRONMENT.lower() == "development"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# Global settings instance
|
| 74 |
+
settings = Settings()
|
app/core/exceptions.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Custom Exceptions and Exception Handlers
|
| 3 |
+
Centralizes error handling logic
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Any, Dict
|
| 8 |
+
|
| 9 |
+
from fastapi import FastAPI, Request, status
|
| 10 |
+
from fastapi.responses import JSONResponse
|
| 11 |
+
from fastapi.exceptions import RequestValidationError, HTTPException
|
| 12 |
+
|
| 13 |
+
from app.core.logging import get_logger
|
| 14 |
+
|
| 15 |
+
logger = get_logger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TaskProcessingError(Exception):
|
| 19 |
+
"""Raised when task processing fails"""
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class AuthenticationError(Exception):
|
| 24 |
+
"""Raised when authentication fails"""
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def create_error_response(
|
| 29 |
+
error_type: str,
|
| 30 |
+
message: str,
|
| 31 |
+
status_code: int,
|
| 32 |
+
details: Any = None
|
| 33 |
+
) -> JSONResponse:
|
| 34 |
+
"""
|
| 35 |
+
Create a standardized error response
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
error_type: Type of error
|
| 39 |
+
message: Error message
|
| 40 |
+
status_code: HTTP status code
|
| 41 |
+
details: Optional additional details
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
JSONResponse: Formatted error response
|
| 45 |
+
"""
|
| 46 |
+
content = {
|
| 47 |
+
"success": False,
|
| 48 |
+
"error": error_type,
|
| 49 |
+
"message": message,
|
| 50 |
+
"timestamp": datetime.now().isoformat()
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if details:
|
| 54 |
+
content["details"] = details
|
| 55 |
+
|
| 56 |
+
return JSONResponse(
|
| 57 |
+
status_code=status_code,
|
| 58 |
+
content=content
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def register_exception_handlers(app: FastAPI):
|
| 63 |
+
"""
|
| 64 |
+
Register all exception handlers with the FastAPI app
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
app: FastAPI application instance
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
@app.exception_handler(RequestValidationError)
|
| 71 |
+
async def validation_exception_handler(
|
| 72 |
+
request: Request,
|
| 73 |
+
exc: RequestValidationError
|
| 74 |
+
):
|
| 75 |
+
"""Handle Pydantic validation errors (HTTP 400)"""
|
| 76 |
+
error_details = exc.errors()
|
| 77 |
+
logger.error(f"❌ Validation Error: {error_details}")
|
| 78 |
+
|
| 79 |
+
# Extract readable error messages
|
| 80 |
+
error_messages = []
|
| 81 |
+
for error in error_details:
|
| 82 |
+
field = " -> ".join(str(loc) for loc in error['loc'])
|
| 83 |
+
message = error['msg']
|
| 84 |
+
error_messages.append(f"{field}: {message}")
|
| 85 |
+
|
| 86 |
+
return create_error_response(
|
| 87 |
+
error_type="ValidationError",
|
| 88 |
+
message="Invalid request format. " + "; ".join(error_messages),
|
| 89 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 90 |
+
details=error_details
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
@app.exception_handler(HTTPException)
|
| 94 |
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
| 95 |
+
"""Handle HTTP exceptions"""
|
| 96 |
+
logger.error(f"❌ HTTP {exc.status_code}: {exc.detail}")
|
| 97 |
+
|
| 98 |
+
return create_error_response(
|
| 99 |
+
error_type=f"HTTP{exc.status_code}",
|
| 100 |
+
message=exc.detail,
|
| 101 |
+
status_code=exc.status_code
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
@app.exception_handler(TaskProcessingError)
|
| 105 |
+
async def task_processing_exception_handler(
|
| 106 |
+
request: Request,
|
| 107 |
+
exc: TaskProcessingError
|
| 108 |
+
):
|
| 109 |
+
"""Handle task processing errors"""
|
| 110 |
+
logger.error(f"❌ Task Processing Error: {str(exc)}", exc_info=True)
|
| 111 |
+
|
| 112 |
+
return create_error_response(
|
| 113 |
+
error_type="TaskProcessingError",
|
| 114 |
+
message=str(exc),
|
| 115 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
@app.exception_handler(AuthenticationError)
|
| 119 |
+
async def authentication_exception_handler(
|
| 120 |
+
request: Request,
|
| 121 |
+
exc: AuthenticationError
|
| 122 |
+
):
|
| 123 |
+
"""Handle authentication errors"""
|
| 124 |
+
logger.warning(f"🚫 Authentication Error: {str(exc)}")
|
| 125 |
+
|
| 126 |
+
return create_error_response(
|
| 127 |
+
error_type="AuthenticationError",
|
| 128 |
+
message=str(exc),
|
| 129 |
+
status_code=status.HTTP_403_FORBIDDEN
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
@app.exception_handler(Exception)
|
| 133 |
+
async def general_exception_handler(request: Request, exc: Exception):
|
| 134 |
+
"""Catch-all handler for unexpected exceptions"""
|
| 135 |
+
logger.error(
|
| 136 |
+
f"❌ Unhandled Exception: {str(exc)}",
|
| 137 |
+
exc_info=True
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
return create_error_response(
|
| 141 |
+
error_type="InternalServerError",
|
| 142 |
+
message="An unexpected error occurred while processing your request",
|
| 143 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
| 144 |
+
)
|
app/core/logging.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Production Logging with Better Stack (Logtail)
|
| 3 |
+
Persistent cloud logging for evaluation and debugging
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import logging
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from app.core.config import settings
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def setup_logging():
|
| 15 |
+
"""
|
| 16 |
+
Configure logging with cloud persistence via Better Stack
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
# Base formatter
|
| 20 |
+
formatter = logging.Formatter(
|
| 21 |
+
fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s',
|
| 22 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Configure root logger
|
| 26 |
+
root_logger = logging.getLogger()
|
| 27 |
+
root_logger.setLevel(getattr(logging, settings.LOG_LEVEL.upper()))
|
| 28 |
+
root_logger.handlers.clear()
|
| 29 |
+
|
| 30 |
+
# Console handler (for HF Spaces logs viewer)
|
| 31 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
| 32 |
+
console_handler.setLevel(logging.INFO)
|
| 33 |
+
console_handler.setFormatter(formatter)
|
| 34 |
+
root_logger.addHandler(console_handler)
|
| 35 |
+
|
| 36 |
+
# Better Stack handler for persistent logging
|
| 37 |
+
if settings.LOGTAIL_SOURCE_TOKEN:
|
| 38 |
+
try:
|
| 39 |
+
from logtail import LogtailHandler
|
| 40 |
+
|
| 41 |
+
logtail_handler = LogtailHandler(source_token=settings.LOGTAIL_SOURCE_TOKEN)
|
| 42 |
+
logtail_handler.setLevel(logging.INFO)
|
| 43 |
+
logtail_handler.setFormatter(formatter)
|
| 44 |
+
root_logger.addHandler(logtail_handler)
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
logger.info("✅ Better Stack logging enabled - logs will persist")
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger = logging.getLogger(__name__)
|
| 50 |
+
logger.warning(f"⚠️ Failed to initialize Better Stack: {e}")
|
| 51 |
+
else:
|
| 52 |
+
logger = logging.getLogger(__name__)
|
| 53 |
+
logger.warning("⚠️ LOGTAIL_SOURCE_TOKEN not set - logs will not persist")
|
| 54 |
+
|
| 55 |
+
# Reduce third-party noise
|
| 56 |
+
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
| 57 |
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 58 |
+
|
| 59 |
+
logger = logging.getLogger(__name__)
|
| 60 |
+
logger.info("Logging initialized for %s", settings.ENVIRONMENT)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def get_logger(name: str) -> logging.Logger:
|
| 64 |
+
"""Get a logger instance"""
|
| 65 |
+
return logging.getLogger(name)
|
app/core/security.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Security Utilities
|
| 3 |
+
Handles authentication and authorization
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from app.core.config import settings
|
| 7 |
+
from app.core.logging import get_logger
|
| 8 |
+
|
| 9 |
+
logger = get_logger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def verify_secret(provided_secret: str) -> bool:
|
| 13 |
+
"""
|
| 14 |
+
Verify the provided secret against environment configuration
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
provided_secret: Secret from request
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
bool: True if secret matches, False otherwise
|
| 21 |
+
"""
|
| 22 |
+
if not settings.is_secret_configured():
|
| 23 |
+
logger.error("⚠️ API_SECRET not configured in environment")
|
| 24 |
+
return False
|
| 25 |
+
|
| 26 |
+
is_valid = provided_secret == settings.API_SECRET
|
| 27 |
+
|
| 28 |
+
if is_valid:
|
| 29 |
+
logger.info("✅ Secret verification successful")
|
| 30 |
+
else:
|
| 31 |
+
logger.warning("🚫 Secret verification failed")
|
| 32 |
+
logger.debug(
|
| 33 |
+
f"Expected length: {len(settings.API_SECRET)}, "
|
| 34 |
+
f"Got length: {len(provided_secret)}"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
return is_valid
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def mask_secret(secret: str, visible_chars: int = 4) -> str:
|
| 41 |
+
"""
|
| 42 |
+
Mask secret for logging purposes
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
secret: Secret to mask
|
| 46 |
+
visible_chars: Number of characters to show at the end
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
str: Masked secret
|
| 50 |
+
"""
|
| 51 |
+
if not secret:
|
| 52 |
+
return ""
|
| 53 |
+
|
| 54 |
+
if len(secret) <= visible_chars:
|
| 55 |
+
return "*" * len(secret)
|
| 56 |
+
|
| 57 |
+
return "*" * (len(secret) - visible_chars) + secret[-visible_chars:]
|
app/main.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main FastAPI Application Entry Point
|
| 3 |
+
Initializes the FastAPI app with all configurations, middleware, and routes
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import FastAPI
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from contextlib import asynccontextmanager
|
| 9 |
+
|
| 10 |
+
from app.core.config import settings
|
| 11 |
+
from app.core.logging import setup_logging, get_logger
|
| 12 |
+
from app.core.exceptions import register_exception_handlers
|
| 13 |
+
from app.middleware.logging import LoggingMiddleware
|
| 14 |
+
from app.api.routes import task, health
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Initialize logger
|
| 18 |
+
logger = get_logger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@asynccontextmanager
|
| 22 |
+
async def lifespan(app: FastAPI):
|
| 23 |
+
"""
|
| 24 |
+
Application lifespan manager for startup and shutdown events
|
| 25 |
+
"""
|
| 26 |
+
# Startup
|
| 27 |
+
logger.info("=" * 80)
|
| 28 |
+
logger.info("🚀 Starting LLM Analysis Quiz API")
|
| 29 |
+
logger.info(f"Environment: {settings.ENVIRONMENT}")
|
| 30 |
+
logger.info(f"Version: {settings.APP_VERSION}")
|
| 31 |
+
logger.info(f"Secret configured: {settings.is_secret_configured()}")
|
| 32 |
+
logger.info("=" * 80)
|
| 33 |
+
|
| 34 |
+
if not settings.is_secret_configured():
|
| 35 |
+
logger.warning("⚠️ WARNING: API_SECRET not configured!")
|
| 36 |
+
|
| 37 |
+
yield
|
| 38 |
+
|
| 39 |
+
# Shutdown
|
| 40 |
+
logger.info("=" * 80)
|
| 41 |
+
logger.info("🛑 Shutting down LLM Analysis Quiz API")
|
| 42 |
+
logger.info("=" * 80)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def create_application() -> FastAPI:
|
| 47 |
+
"""
|
| 48 |
+
Application factory pattern for creating FastAPI instance
|
| 49 |
+
"""
|
| 50 |
+
app = FastAPI(
|
| 51 |
+
title=settings.APP_NAME,
|
| 52 |
+
description=settings.APP_DESCRIPTION,
|
| 53 |
+
version=settings.APP_VERSION,
|
| 54 |
+
lifespan=lifespan,
|
| 55 |
+
docs_url="/docs" if settings.ENVIRONMENT == "development" else None,
|
| 56 |
+
redoc_url="/redoc" if settings.ENVIRONMENT == "development" else None,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Configure CORS
|
| 60 |
+
app.add_middleware(
|
| 61 |
+
CORSMiddleware,
|
| 62 |
+
allow_origins=settings.ALLOWED_ORIGINS,
|
| 63 |
+
allow_credentials=True,
|
| 64 |
+
allow_methods=["*"],
|
| 65 |
+
allow_headers=["*"],
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# Add custom middleware
|
| 69 |
+
app.add_middleware(LoggingMiddleware)
|
| 70 |
+
|
| 71 |
+
# Register exception handlers
|
| 72 |
+
register_exception_handlers(app)
|
| 73 |
+
|
| 74 |
+
# Include routers
|
| 75 |
+
app.include_router(health.router, tags=["Health"])
|
| 76 |
+
app.include_router(task.router, tags=["Tasks"])
|
| 77 |
+
|
| 78 |
+
return app
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# Initialize logging
|
| 82 |
+
setup_logging()
|
| 83 |
+
|
| 84 |
+
# Create app instance
|
| 85 |
+
app = create_application()
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
if __name__ == "__main__":
|
| 89 |
+
import uvicorn
|
| 90 |
+
|
| 91 |
+
logger.info(f"Starting server on {settings.HOST}:{settings.PORT}")
|
| 92 |
+
|
| 93 |
+
uvicorn.run(
|
| 94 |
+
"app.main:app",
|
| 95 |
+
host=settings.HOST,
|
| 96 |
+
port=settings.PORT,
|
| 97 |
+
reload=settings.ENVIRONMENT == "development",
|
| 98 |
+
log_level="info"
|
| 99 |
+
)
|
app/middleware/__pycache__/logging.cpython-313.pyc
ADDED
|
Binary file (2.21 kB). View file
|
|
|
app/middleware/logging.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Logging Middleware
|
| 3 |
+
Logs all incoming requests and responses
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import time
|
| 7 |
+
from fastapi import Request
|
| 8 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 9 |
+
|
| 10 |
+
from app.core.logging import get_logger
|
| 11 |
+
|
| 12 |
+
logger = get_logger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class LoggingMiddleware(BaseHTTPMiddleware):
|
| 16 |
+
"""
|
| 17 |
+
Middleware to log all HTTP requests and responses
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
async def dispatch(self, request: Request, call_next):
|
| 21 |
+
"""
|
| 22 |
+
Process each request and log details
|
| 23 |
+
"""
|
| 24 |
+
start_time = time.time()
|
| 25 |
+
|
| 26 |
+
# Log incoming request
|
| 27 |
+
logger.info(f"📥 {request.method} {request.url.path}")
|
| 28 |
+
logger.debug(f"Client: {request.client.host if request.client else 'Unknown'}")
|
| 29 |
+
|
| 30 |
+
# Process request
|
| 31 |
+
try:
|
| 32 |
+
response = await call_next(request)
|
| 33 |
+
|
| 34 |
+
# Calculate execution time
|
| 35 |
+
execution_time = time.time() - start_time
|
| 36 |
+
|
| 37 |
+
# Log response
|
| 38 |
+
logger.info(
|
| 39 |
+
f"📤 Response: {response.status_code} | "
|
| 40 |
+
f"Time: {execution_time:.3f}s"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
return response
|
| 44 |
+
|
| 45 |
+
except Exception as e:
|
| 46 |
+
execution_time = time.time() - start_time
|
| 47 |
+
logger.error(
|
| 48 |
+
f"❌ Request failed after {execution_time:.3f}s: {str(e)}",
|
| 49 |
+
exc_info=True
|
| 50 |
+
)
|
| 51 |
+
raise
|
app/models/__pycache__/request.cpython-313.pyc
ADDED
|
Binary file (2.34 kB). View file
|
|
|
app/models/__pycache__/response.cpython-313.pyc
ADDED
|
Binary file (2.95 kB). View file
|
|
|
app/models/request.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Request Models (Pydantic Schemas)
|
| 3 |
+
Defines the structure of incoming requests
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Optional, Dict, Any
|
| 7 |
+
from pydantic import BaseModel, Field, EmailStr, HttpUrl, validator
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TaskRequest(BaseModel):
|
| 11 |
+
"""
|
| 12 |
+
Schema for task request validation
|
| 13 |
+
"""
|
| 14 |
+
email: EmailStr = Field(
|
| 15 |
+
...,
|
| 16 |
+
description="Student email ID",
|
| 17 |
+
examples=["[email protected]"]
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
secret: str = Field(
|
| 21 |
+
...,
|
| 22 |
+
min_length=1,
|
| 23 |
+
description="Student-provided secret for authentication",
|
| 24 |
+
examples=["my-secret-key-123"]
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
url: HttpUrl = Field(
|
| 28 |
+
...,
|
| 29 |
+
description="Unique task URL containing the quiz question",
|
| 30 |
+
examples=["https://example.com/quiz-834"]
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
task_description: Optional[str] = Field(
|
| 34 |
+
None,
|
| 35 |
+
description="Optional description of the task to perform"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
additional_params: Optional[Dict[str, Any]] = Field(
|
| 39 |
+
None,
|
| 40 |
+
description="Optional additional parameters for task execution"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
@validator('secret')
|
| 44 |
+
def secret_not_empty(cls, v):
|
| 45 |
+
"""Ensure secret is not just whitespace"""
|
| 46 |
+
if not v or not v.strip():
|
| 47 |
+
raise ValueError('Secret cannot be empty or whitespace')
|
| 48 |
+
return v.strip()
|
| 49 |
+
|
| 50 |
+
class Config:
|
| 51 |
+
json_schema_extra = {
|
| 52 |
+
"example": {
|
| 53 |
+
"email": "[email protected]",
|
| 54 |
+
"secret": "your-secret-key",
|
| 55 |
+
"url": "https://example.com/quiz-834",
|
| 56 |
+
"task_description": "Scrape the webpage and analyze data"
|
| 57 |
+
}
|
| 58 |
+
}
|
app/models/response.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Response Models (Pydantic Schemas)
|
| 3 |
+
Defines the structure of API responses
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Optional, Dict, Any
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from pydantic import BaseModel, Field
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TaskResponse(BaseModel):
|
| 12 |
+
"""
|
| 13 |
+
Schema for successful task responses
|
| 14 |
+
"""
|
| 15 |
+
success: bool = Field(
|
| 16 |
+
...,
|
| 17 |
+
description="Whether the task completed successfully"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
message: str = Field(
|
| 21 |
+
...,
|
| 22 |
+
description="Status message"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
data: Optional[Dict[str, Any]] = Field(
|
| 26 |
+
None,
|
| 27 |
+
description="Task result data"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
email: Optional[str] = Field(
|
| 31 |
+
None,
|
| 32 |
+
description="Email from request"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
task_url: Optional[str] = Field(
|
| 36 |
+
None,
|
| 37 |
+
description="Task URL processed"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
execution_time: Optional[float] = Field(
|
| 41 |
+
None,
|
| 42 |
+
description="Execution time in seconds"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
timestamp: str = Field(
|
| 46 |
+
default_factory=lambda: datetime.now().isoformat(),
|
| 47 |
+
description="Response timestamp"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
class Config:
|
| 51 |
+
json_schema_extra = {
|
| 52 |
+
"example": {
|
| 53 |
+
"success": True,
|
| 54 |
+
"message": "Task completed successfully",
|
| 55 |
+
"data": {"result": "Analysis complete"},
|
| 56 |
+
"email": "[email protected]",
|
| 57 |
+
"task_url": "https://example.com/quiz-834",
|
| 58 |
+
"execution_time": 2.34,
|
| 59 |
+
"timestamp": "2025-11-27T23:48:00"
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class HealthResponse(BaseModel):
|
| 65 |
+
"""
|
| 66 |
+
Schema for health check responses
|
| 67 |
+
"""
|
| 68 |
+
status: str = Field(..., description="Service status")
|
| 69 |
+
timestamp: str = Field(
|
| 70 |
+
default_factory=lambda: datetime.now().isoformat()
|
| 71 |
+
)
|
| 72 |
+
environment: Optional[str] = Field(None)
|
| 73 |
+
version: Optional[str] = Field(None)
|
| 74 |
+
secret_configured: Optional[bool] = Field(None)
|
app/services/__pycache__/task_processor.cpython-313.pyc
ADDED
|
Binary file (2.4 kB). View file
|
|
|
app/services/task_processor.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task Processing Service
|
| 3 |
+
Business logic for processing tasks
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
|
| 8 |
+
from app.models.request import TaskRequest
|
| 9 |
+
from app.core.logging import get_logger
|
| 10 |
+
from app.core.exceptions import TaskProcessingError
|
| 11 |
+
|
| 12 |
+
logger = get_logger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TaskProcessor:
|
| 16 |
+
"""
|
| 17 |
+
Service class for processing tasks
|
| 18 |
+
Handles the core business logic
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self):
|
| 22 |
+
logger.debug("TaskProcessor initialized")
|
| 23 |
+
|
| 24 |
+
async def process(self, task_data: TaskRequest) -> Dict[str, Any]:
|
| 25 |
+
"""
|
| 26 |
+
Process a task based on the provided data
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
task_data: Validated task request
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Dict containing task results
|
| 33 |
+
|
| 34 |
+
Raises:
|
| 35 |
+
TaskProcessingError: If processing fails
|
| 36 |
+
"""
|
| 37 |
+
logger.info(f"Processing task for: {task_data.email}")
|
| 38 |
+
logger.info(f"Task URL: {task_data.url}")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
# TODO: Implement actual task processing logic
|
| 42 |
+
# This will integrate with orchestrator and various modules
|
| 43 |
+
|
| 44 |
+
result = {
|
| 45 |
+
"status": "processed",
|
| 46 |
+
"task_url": str(task_data.url),
|
| 47 |
+
"message": "Task processing logic to be implemented",
|
| 48 |
+
"email": task_data.email
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
logger.info("✅ Task processed successfully")
|
| 52 |
+
return result
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
logger.error(f"❌ Task processing failed: {str(e)}", exc_info=True)
|
| 56 |
+
raise TaskProcessingError(f"Failed to process task: {str(e)}")
|
dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
|
| 5 |
+
WORKDIR /home/user/app
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# Install dependencies
|
| 9 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 10 |
+
|
| 11 |
+
RUN pip install -r requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY --chown=user . .
|
| 14 |
+
|
| 15 |
+
USER user
|
| 16 |
+
ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH
|
| 17 |
+
|
| 18 |
+
# Expose port
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# Run with production settings
|
| 22 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core Framework
|
| 2 |
+
fastapi==0.109.0
|
| 3 |
+
uvicorn[standard]==0.27.0
|
| 4 |
+
pydantic
|
| 5 |
+
pydantic-settings
|
| 6 |
+
pydantic[email]
|
| 7 |
+
python-dotenv==1.0.0
|
| 8 |
+
logtail-python
|
| 9 |
+
|
| 10 |
+
# HTTP Clients
|
| 11 |
+
httpx==0.26.0
|
| 12 |
+
requests==2.31.0
|
| 13 |
+
|
| 14 |
+
# Data Processing
|
| 15 |
+
# pandas==2.2.0
|
| 16 |
+
# numpy==1.26.3
|
| 17 |
+
|
| 18 |
+
# Add more as you build modules
|