implement core api
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +0 -26
- .env.example +77 -26
- .gitignore +2 -0
- .kiro/specs/fastapi-backend/design.md +1055 -0
- .kiro/specs/fastapi-backend/requirements.md +126 -0
- .kiro/specs/fastapi-backend/tasks.md +305 -0
- API_DOCUMENTATION.md +1311 -0
- AUTHENTICATION_GUIDE.md +1120 -0
- CLERK_TOKEN_GUIDE.md +122 -0
- Dockerfile +15 -92
- FASTAPI_SETUP_GUIDE.md +302 -0
- Makefile +278 -0
- README.md +158 -259
- README_API_TESTING.md +258 -0
- SYSTEM_OVERVIEW.md +399 -0
- WORKING_SETUP.md +131 -0
- auth_token.txt +1 -0
- docker-compose.dev.yml +0 -0
- docker-compose.simple.yml +34 -0
- docker-compose.test.yml +68 -0
- docker-compose.yml +0 -80
- docs/client-generation.md +525 -0
- extract_token.html +279 -0
- fastapi-backend-pyproject.toml +179 -0
- main.py +6 -0
- ngrok-dev.yml +12 -0
- ngrok.yml +12 -0
- openapi-generator-config.yaml +183 -0
- pyproject.toml +122 -0
- quick_api_test.py +72 -0
- requirements-test.txt +3 -0
- requirements.txt +35 -1
- run_api_tests.py +54 -0
- run_debug.py +50 -0
- run_video_test.py +57 -0
- scripts/generate_clients.py +712 -0
- scripts/generate_clients.sh +557 -0
- scripts/setup.py +106 -0
- scripts/test_clients.py +579 -0
- setup-dev.bat +90 -0
- setup-dev.sh +108 -0
- simple_app.py +76 -0
- simple_config.py +48 -0
- src/app/__init__.py +1 -0
- src/app/api/__init__.py +1 -0
- src/app/api/dependencies.py +490 -0
- src/app/api/v1/__init__.py +1 -0
- src/app/api/v1/auth.py +392 -0
- src/app/api/v1/files.py +978 -0
- src/app/api/v1/jobs.py +532 -0
.env
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
# OpenAI
|
| 2 |
-
OPENAI_API_KEY=""
|
| 3 |
-
|
| 4 |
-
# Azure OpenAI
|
| 5 |
-
AZURE_API_KEY=""
|
| 6 |
-
AZURE_API_BASE=""
|
| 7 |
-
AZURE_API_VERSION=""
|
| 8 |
-
OPENROUTER_API_KEY = "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"
|
| 9 |
-
# Google Vertex AI
|
| 10 |
-
VERTEXAI_PROJECT=""
|
| 11 |
-
VERTEXAI_LOCATION=""
|
| 12 |
-
GOOGLE_APPLICATION_CREDENTIALS=""
|
| 13 |
-
GITHUB_API_KEY = "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"
|
| 14 |
-
GITHUB_TOKEN = "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"
|
| 15 |
-
OPENAI_API_KEY = "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"
|
| 16 |
-
# Google Gemini
|
| 17 |
-
GEMINI_API_KEY="AIzaSyBUCGQ_hDLAHQN-T1ycWBJV8SGfwusfEjg"
|
| 18 |
-
|
| 19 |
-
...
|
| 20 |
-
|
| 21 |
-
# Kokoro TTS Settings
|
| 22 |
-
KOKORO_MODEL_PATH="models/kokoro-v0_19.onnx"
|
| 23 |
-
KOKORO_VOICES_PATH="models/voices.bin"
|
| 24 |
-
KOKORO_DEFAULT_VOICE="af"
|
| 25 |
-
KOKORO_DEFAULT_SPEED="1.0"
|
| 26 |
-
KOKORO_DEFAULT_LANG="en-us"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.env.example
CHANGED
|
@@ -1,26 +1,77 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
#
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FastAPI Video Backend Environment Configuration
|
| 2 |
+
# Copy this file to .env and update the values
|
| 3 |
+
|
| 4 |
+
# Application Settings
|
| 5 |
+
APP_NAME="FastAPI Video Backend"
|
| 6 |
+
APP_VERSION="0.1.0"
|
| 7 |
+
DEBUG=true
|
| 8 |
+
ENVIRONMENT=development
|
| 9 |
+
|
| 10 |
+
# Server Settings
|
| 11 |
+
HOST=0.0.0.0
|
| 12 |
+
PORT=8000
|
| 13 |
+
RELOAD=true
|
| 14 |
+
|
| 15 |
+
# API Settings
|
| 16 |
+
API_V1_PREFIX="/api/v1"
|
| 17 |
+
DOCS_URL="/docs"
|
| 18 |
+
REDOC_URL="/redoc"
|
| 19 |
+
OPENAPI_URL="/openapi.json"
|
| 20 |
+
|
| 21 |
+
# CORS Settings
|
| 22 |
+
ALLOWED_ORIGINS="http://localhost:3000,http://localhost:8080,http://127.0.0.1:3000"
|
| 23 |
+
ALLOWED_METHODS="GET,POST,PUT,DELETE,OPTIONS"
|
| 24 |
+
ALLOWED_HEADERS="*"
|
| 25 |
+
|
| 26 |
+
# Redis Settings
|
| 27 |
+
REDIS_URL="redis://localhost:6379/0"
|
| 28 |
+
REDIS_HOST=localhost
|
| 29 |
+
REDIS_PORT=6379
|
| 30 |
+
REDIS_DB=0
|
| 31 |
+
REDIS_PASSWORD=
|
| 32 |
+
REDIS_MAX_CONNECTIONS=20
|
| 33 |
+
REDIS_SOCKET_TIMEOUT=5
|
| 34 |
+
REDIS_SOCKET_CONNECT_TIMEOUT=5
|
| 35 |
+
|
| 36 |
+
# Clerk Authentication Settings (REQUIRED)
|
| 37 |
+
CLERK_SECRET_KEY=your_clerk_secret_key_here
|
| 38 |
+
CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here
|
| 39 |
+
CLERK_WEBHOOK_SECRET=your_clerk_webhook_secret_here
|
| 40 |
+
CLERK_JWT_VERIFICATION=true
|
| 41 |
+
|
| 42 |
+
# Job Queue Settings
|
| 43 |
+
JOB_QUEUE_NAME=video_generation_queue
|
| 44 |
+
JOB_QUEUE_MAX_SIZE=1000
|
| 45 |
+
JOB_DEFAULT_TIMEOUT=3600
|
| 46 |
+
JOB_RETRY_ATTEMPTS=3
|
| 47 |
+
|
| 48 |
+
# File Storage Settings
|
| 49 |
+
UPLOAD_DIR=./uploads
|
| 50 |
+
MAX_FILE_SIZE=104857600
|
| 51 |
+
ALLOWED_FILE_TYPES="image/jpeg,image/png,image/gif,video/mp4,text/plain"
|
| 52 |
+
|
| 53 |
+
# Rate Limiting Settings
|
| 54 |
+
RATE_LIMIT_REQUESTS=100
|
| 55 |
+
RATE_LIMIT_WINDOW=60
|
| 56 |
+
RATE_LIMIT_PER_USER=50
|
| 57 |
+
|
| 58 |
+
# Logging Settings
|
| 59 |
+
LOG_LEVEL=INFO
|
| 60 |
+
LOG_FORMAT=json
|
| 61 |
+
LOG_FILE=
|
| 62 |
+
LOG_ROTATION="1 day"
|
| 63 |
+
LOG_RETENTION="30 days"
|
| 64 |
+
|
| 65 |
+
# Security Settings (REQUIRED)
|
| 66 |
+
SECRET_KEY=your_super_secret_key_here_change_in_production
|
| 67 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
| 68 |
+
REFRESH_TOKEN_EXPIRE_DAYS=7
|
| 69 |
+
|
| 70 |
+
# Video Generation Settings
|
| 71 |
+
VIDEO_OUTPUT_DIR=./videos
|
| 72 |
+
VIDEO_QUALITY_DEFAULT=medium
|
| 73 |
+
VIDEO_MAX_DURATION=600
|
| 74 |
+
|
| 75 |
+
# Health Check Settings
|
| 76 |
+
HEALTH_CHECK_INTERVAL=30
|
| 77 |
+
HEALTH_CHECK_TIMEOUT=5
|
.gitignore
CHANGED
|
@@ -9,6 +9,8 @@ __pycache__/
|
|
| 9 |
# Distribution / packaging
|
| 10 |
.Python
|
| 11 |
env/
|
|
|
|
|
|
|
| 12 |
build/
|
| 13 |
develop-eggs/
|
| 14 |
dist/
|
|
|
|
| 9 |
# Distribution / packaging
|
| 10 |
.Python
|
| 11 |
env/
|
| 12 |
+
.env
|
| 13 |
+
.*env
|
| 14 |
build/
|
| 15 |
develop-eggs/
|
| 16 |
dist/
|
.kiro/specs/fastapi-backend/design.md
ADDED
|
@@ -0,0 +1,1055 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design Document
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This document outlines the design for a simplified FastAPI backend that serves as the primary interface for the multi-agent video generation system. The backend uses Pydantic for all data modeling and validation, Clerk for authentication, and Redis for both caching and job queuing. The design emphasizes simplicity and rapid development while maintaining clean architecture principles.
|
| 6 |
+
|
| 7 |
+
The system provides REST API endpoints for video generation requests and job management, with Redis handling both the job queue and caching layer. Authentication is managed entirely through Clerk, eliminating the need for custom user management.
|
| 8 |
+
|
| 9 |
+
## Architecture
|
| 10 |
+
|
| 11 |
+
### High-Level Architecture
|
| 12 |
+
|
| 13 |
+
```mermaid
|
| 14 |
+
graph TB
|
| 15 |
+
subgraph "Client Layer"
|
| 16 |
+
WEB[Web Frontend]
|
| 17 |
+
MOBILE[Mobile App]
|
| 18 |
+
API_CLIENT[API Clients]
|
| 19 |
+
end
|
| 20 |
+
|
| 21 |
+
subgraph "API Gateway Layer"
|
| 22 |
+
NGINX[Nginx Reverse Proxy]
|
| 23 |
+
RATE_LIMIT[Rate Limiting]
|
| 24 |
+
AUTH_MW[Authentication Middleware]
|
| 25 |
+
end
|
| 26 |
+
|
| 27 |
+
subgraph "FastAPI Application"
|
| 28 |
+
API_ROUTER[API Routers]
|
| 29 |
+
WEBSOCKET[WebSocket Handlers]
|
| 30 |
+
MIDDLEWARE[Custom Middleware]
|
| 31 |
+
DEPS[Dependencies]
|
| 32 |
+
end
|
| 33 |
+
|
| 34 |
+
subgraph "Business Logic Layer"
|
| 35 |
+
VIDEO_SERVICE[Video Generation Service]
|
| 36 |
+
JOB_SERVICE[Job Management Service]
|
| 37 |
+
FILE_SERVICE[File Management Service]
|
| 38 |
+
NOTIFICATION_SERVICE[Notification Service]
|
| 39 |
+
end
|
| 40 |
+
|
| 41 |
+
subgraph "Data Layer"
|
| 42 |
+
REDIS[(Redis)]
|
| 43 |
+
FILE_STORAGE[Local File Storage]
|
| 44 |
+
end
|
| 45 |
+
|
| 46 |
+
subgraph "External Services"
|
| 47 |
+
CLERK[Clerk Authentication]
|
| 48 |
+
end
|
| 49 |
+
|
| 50 |
+
subgraph "External Systems"
|
| 51 |
+
VIDEO_PIPELINE[Multi-Agent Video Pipeline]
|
| 52 |
+
MONITORING[Monitoring & Logging]
|
| 53 |
+
end
|
| 54 |
+
|
| 55 |
+
WEB --> NGINX
|
| 56 |
+
MOBILE --> NGINX
|
| 57 |
+
API_CLIENT --> NGINX
|
| 58 |
+
|
| 59 |
+
NGINX --> RATE_LIMIT
|
| 60 |
+
RATE_LIMIT --> AUTH_MW
|
| 61 |
+
AUTH_MW --> API_ROUTER
|
| 62 |
+
AUTH_MW --> WEBSOCKET
|
| 63 |
+
|
| 64 |
+
API_ROUTER --> MIDDLEWARE
|
| 65 |
+
WEBSOCKET --> MIDDLEWARE
|
| 66 |
+
MIDDLEWARE --> DEPS
|
| 67 |
+
|
| 68 |
+
DEPS --> VIDEO_SERVICE
|
| 69 |
+
DEPS --> JOB_SERVICE
|
| 70 |
+
DEPS --> FILE_SERVICE
|
| 71 |
+
DEPS --> NOTIFICATION_SERVICE
|
| 72 |
+
|
| 73 |
+
VIDEO_SERVICE --> REDIS
|
| 74 |
+
JOB_SERVICE --> REDIS
|
| 75 |
+
FILE_SERVICE --> FILE_STORAGE
|
| 76 |
+
NOTIFICATION_SERVICE --> REDIS
|
| 77 |
+
|
| 78 |
+
AUTH_MW --> CLERK
|
| 79 |
+
|
| 80 |
+
QUEUE --> VIDEO_PIPELINE
|
| 81 |
+
VIDEO_PIPELINE --> MONITORING
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### Project Structure
|
| 85 |
+
|
| 86 |
+
Simplified structure focusing on Pydantic models and Redis:
|
| 87 |
+
|
| 88 |
+
```
|
| 89 |
+
src/
|
| 90 |
+
├── app/
|
| 91 |
+
│ ├── main.py # FastAPI application entry point
|
| 92 |
+
│ ├── api/ # API layer
|
| 93 |
+
│ │ ├── dependencies.py # Shared dependencies (Clerk auth, Redis)
|
| 94 |
+
│ │ └── v1/ # API version 1
|
| 95 |
+
│ │ ├── __init__.py
|
| 96 |
+
│ │ ├── videos.py # Video generation endpoints
|
| 97 |
+
│ │ ├── jobs.py # Job management endpoints
|
| 98 |
+
│ │ └── system.py # System health endpoints
|
| 99 |
+
│ ├── core/ # Core utilities and configurations
|
| 100 |
+
│ │ ├── config.py # Application settings
|
| 101 |
+
│ │ ├── redis.py # Redis connection and utilities
|
| 102 |
+
│ │ ├── auth.py # Clerk authentication utilities
|
| 103 |
+
│ │ ├── logger.py # Logging configuration
|
| 104 |
+
│ │ └── exceptions.py # Custom exceptions
|
| 105 |
+
│ ├── services/ # Business logic layer
|
| 106 |
+
│ │ ├── video_service.py # Video generation business logic
|
| 107 |
+
│ │ ├── job_service.py # Job management logic
|
| 108 |
+
│ │ └── queue_service.py # Redis queue management
|
| 109 |
+
│ ├── models/ # Pydantic models only
|
| 110 |
+
│ │ ├── __init__.py
|
| 111 |
+
│ │ ├── job.py # Job data models
|
| 112 |
+
│ │ ├── video.py # Video metadata models
|
| 113 |
+
│ │ ├── user.py # User data models (from Clerk)
|
| 114 |
+
│ │ └── system.py # System status models
|
| 115 |
+
│ ├── middleware/ # Custom middleware
|
| 116 |
+
│ │ ├── __init__.py
|
| 117 |
+
│ │ ├── cors.py # CORS middleware
|
| 118 |
+
│ │ ├── clerk_auth.py # Clerk authentication middleware
|
| 119 |
+
│ │ └── error_handling.py # Global error handling
|
| 120 |
+
│ └── utils/ # Utility functions
|
| 121 |
+
│ ├── __init__.py
|
| 122 |
+
│ ├── file_utils.py # File handling utilities
|
| 123 |
+
│ └── helpers.py # General helper functions
|
| 124 |
+
├── tests/ # Test suite
|
| 125 |
+
│ ├── conftest.py # Test configuration
|
| 126 |
+
│ ├── test_api/ # API endpoint tests
|
| 127 |
+
│ └── test_services/ # Service layer tests
|
| 128 |
+
└── scripts/ # Utility scripts
|
| 129 |
+
└── setup_redis.py # Redis setup script
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## Components and Interfaces
|
| 133 |
+
|
| 134 |
+
### API Layer Components
|
| 135 |
+
|
| 136 |
+
#### 1. Video Generation Router (`api/v1/videos.py`)
|
| 137 |
+
|
| 138 |
+
**Endpoints:**
|
| 139 |
+
- `POST /api/v1/videos/generate` - Submit video generation request
|
| 140 |
+
- `POST /api/v1/videos/batch` - Submit batch video generation requests
|
| 141 |
+
- `GET /api/v1/videos/jobs/{job_id}/status` - Get job status
|
| 142 |
+
- `GET /api/v1/videos/jobs/{job_id}/download` - Download completed video
|
| 143 |
+
- `GET /api/v1/videos/jobs/{job_id}/metadata` - Get job metadata
|
| 144 |
+
|
| 145 |
+
**Key Features:**
|
| 146 |
+
- Request validation using Pydantic schemas
|
| 147 |
+
- Async request handling
|
| 148 |
+
- Integration with job service
|
| 149 |
+
- File streaming for downloads
|
| 150 |
+
- Comprehensive error handling
|
| 151 |
+
|
| 152 |
+
#### 2. Job Management Router (`api/v1/jobs.py`)
|
| 153 |
+
|
| 154 |
+
**Endpoints:**
|
| 155 |
+
- `GET /api/v1/jobs` - List jobs with pagination and filtering
|
| 156 |
+
- `POST /api/v1/jobs/{job_id}/cancel` - Cancel job
|
| 157 |
+
- `DELETE /api/v1/jobs/{job_id}` - Delete job (soft delete)
|
| 158 |
+
- `GET /api/v1/jobs/{job_id}/logs` - Get job processing logs
|
| 159 |
+
|
| 160 |
+
**Key Features:**
|
| 161 |
+
- Pagination support using `fastcrud` patterns
|
| 162 |
+
- Advanced filtering and sorting
|
| 163 |
+
- Job lifecycle management
|
| 164 |
+
- Audit trail maintenance
|
| 165 |
+
|
| 166 |
+
#### 3. User Management Router (`api/v1/users.py`)
|
| 167 |
+
|
| 168 |
+
**Endpoints:**
|
| 169 |
+
- `POST /api/v1/users/register` - User registration
|
| 170 |
+
- `POST /api/v1/users/login` - User authentication
|
| 171 |
+
- `GET /api/v1/users/profile` - Get user profile
|
| 172 |
+
- `PUT /api/v1/users/profile` - Update user profile
|
| 173 |
+
- `POST /api/v1/users/verify-email` - Email verification
|
| 174 |
+
- `POST /api/v1/users/reset-password` - Password reset
|
| 175 |
+
|
| 176 |
+
**Key Features:**
|
| 177 |
+
- JWT-based authentication
|
| 178 |
+
- Email verification workflow
|
| 179 |
+
- Password reset functionality
|
| 180 |
+
- Profile management
|
| 181 |
+
|
| 182 |
+
#### 4. Subscription Management Router (`api/v1/subscriptions.py`)
|
| 183 |
+
|
| 184 |
+
**Endpoints:**
|
| 185 |
+
- `GET /api/v1/subscriptions/plans` - List available subscription plans
|
| 186 |
+
- `POST /api/v1/subscriptions/subscribe` - Create new subscription
|
| 187 |
+
- `GET /api/v1/subscriptions/current` - Get current user subscription
|
| 188 |
+
- `PUT /api/v1/subscriptions/upgrade` - Upgrade subscription plan
|
| 189 |
+
- `POST /api/v1/subscriptions/cancel` - Cancel subscription
|
| 190 |
+
- `GET /api/v1/subscriptions/usage` - Get usage statistics
|
| 191 |
+
|
| 192 |
+
**Key Features:**
|
| 193 |
+
- Subscription plan management
|
| 194 |
+
- Credit tracking and usage monitoring
|
| 195 |
+
- Billing integration
|
| 196 |
+
- Usage analytics
|
| 197 |
+
|
| 198 |
+
#### 5. WebSocket Handler (`api/v1/websockets.py`)
|
| 199 |
+
|
| 200 |
+
**Endpoints:**
|
| 201 |
+
- `WS /ws/jobs/{job_id}` - Real-time job status updates
|
| 202 |
+
- `WS /ws/system/health` - System health monitoring
|
| 203 |
+
|
| 204 |
+
**Key Features:**
|
| 205 |
+
- Connection management
|
| 206 |
+
- Real-time status broadcasting
|
| 207 |
+
- Graceful disconnection handling
|
| 208 |
+
- Authentication for WebSocket connections
|
| 209 |
+
|
| 210 |
+
### Service Layer Components
|
| 211 |
+
|
| 212 |
+
#### 1. Video Generation Service (`services/video_service.py`)
|
| 213 |
+
|
| 214 |
+
**Responsibilities:**
|
| 215 |
+
- Interface with multi-agent video generation pipeline
|
| 216 |
+
- Job queue management
|
| 217 |
+
- Configuration validation
|
| 218 |
+
- Progress tracking
|
| 219 |
+
|
| 220 |
+
**Key Methods:**
|
| 221 |
+
```python
|
| 222 |
+
async def create_video_job(request: VideoGenerationRequest) -> JobResponse
|
| 223 |
+
async def create_batch_jobs(requests: List[VideoGenerationRequest]) -> BatchJobResponse
|
| 224 |
+
async def get_job_status(job_id: str) -> JobStatus
|
| 225 |
+
async def cancel_job(job_id: str) -> bool
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
#### 2. Job Management Service (`services/job_service.py`)
|
| 229 |
+
|
| 230 |
+
**Responsibilities:**
|
| 231 |
+
- Job lifecycle management
|
| 232 |
+
- Status updates and notifications
|
| 233 |
+
- Resource allocation
|
| 234 |
+
- Performance monitoring
|
| 235 |
+
|
| 236 |
+
**Key Methods:**
|
| 237 |
+
```python
|
| 238 |
+
async def update_job_status(job_id: str, status: JobStatus, metadata: dict)
|
| 239 |
+
async def get_jobs_paginated(filters: JobFilters, pagination: PaginationParams) -> PaginatedResponse
|
| 240 |
+
async def cleanup_completed_jobs(retention_days: int)
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
#### 3. File Management Service (`services/file_service.py`)
|
| 244 |
+
|
| 245 |
+
**Responsibilities:**
|
| 246 |
+
- AWS S3 file upload/download handling
|
| 247 |
+
- Storage management with versioning
|
| 248 |
+
- Security scanning and validation
|
| 249 |
+
- Metadata extraction and storage
|
| 250 |
+
|
| 251 |
+
**Key Methods:**
|
| 252 |
+
```python
|
| 253 |
+
async def upload_to_s3(file: UploadFile, user_id: str, file_type: str) -> S3FileMetadata
|
| 254 |
+
async def download_from_s3(s3_key: str, bucket: str) -> StreamingResponse
|
| 255 |
+
async def generate_presigned_url(s3_key: str, expiration: int = 3600) -> str
|
| 256 |
+
async def validate_file(file: UploadFile) -> ValidationResult
|
| 257 |
+
async def cleanup_expired_files()
|
| 258 |
+
async def create_video_thumbnail(video_s3_key: str) -> str
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
#### 4. Subscription Service (`services/subscription_service.py`)
|
| 262 |
+
|
| 263 |
+
**Responsibilities:**
|
| 264 |
+
- Subscription lifecycle management
|
| 265 |
+
- Credit tracking and usage monitoring
|
| 266 |
+
- Billing integration
|
| 267 |
+
- Plan upgrade/downgrade logic
|
| 268 |
+
|
| 269 |
+
**Key Methods:**
|
| 270 |
+
```python
|
| 271 |
+
async def create_subscription(user_id: str, plan_id: int, payment_method: str) -> Subscription
|
| 272 |
+
async def check_user_credits(user_id: str) -> int
|
| 273 |
+
async def consume_credits(user_id: str, credits: int) -> bool
|
| 274 |
+
async def upgrade_subscription(user_id: str, new_plan_id: int) -> Subscription
|
| 275 |
+
async def cancel_subscription(user_id: str) -> bool
|
| 276 |
+
async def process_billing_cycle()
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
#### 5. AWS Integration Service (`services/aws_service.py`)
|
| 280 |
+
|
| 281 |
+
**Responsibilities:**
|
| 282 |
+
- AWS service integration and management
|
| 283 |
+
- DynamoDB operations for high-frequency data
|
| 284 |
+
- SQS queue management
|
| 285 |
+
- CloudWatch metrics and logging
|
| 286 |
+
|
| 287 |
+
**Key Methods:**
|
| 288 |
+
```python
|
| 289 |
+
async def put_job_status_dynamodb(job_id: str, status: dict)
|
| 290 |
+
async def get_job_status_history(job_id: str) -> List[dict]
|
| 291 |
+
async def send_sqs_message(queue_url: str, message: dict)
|
| 292 |
+
async def put_cloudwatch_metric(metric_name: str, value: float, dimensions: dict)
|
| 293 |
+
async def log_user_activity(user_id: str, activity: dict)
|
| 294 |
+
```
|
| 295 |
+
|
| 296 |
+
### Data Layer Components
|
| 297 |
+
|
| 298 |
+
#### Pydantic Data Models
|
| 299 |
+
|
| 300 |
+
**Job Model (`models/job.py`):**
|
| 301 |
+
```python
|
| 302 |
+
from pydantic import BaseModel, Field
|
| 303 |
+
from datetime import datetime
|
| 304 |
+
from enum import Enum
|
| 305 |
+
from typing import Optional, Dict, Any
|
| 306 |
+
import uuid
|
| 307 |
+
|
| 308 |
+
class JobStatus(str, Enum):
|
| 309 |
+
QUEUED = "queued"
|
| 310 |
+
PROCESSING = "processing"
|
| 311 |
+
COMPLETED = "completed"
|
| 312 |
+
FAILED = "failed"
|
| 313 |
+
CANCELLED = "cancelled"
|
| 314 |
+
|
| 315 |
+
class JobCreate(BaseModel):
|
| 316 |
+
topic: str = Field(..., min_length=1, max_length=500)
|
| 317 |
+
context: str = Field(..., min_length=1, max_length=2000)
|
| 318 |
+
model: Optional[str] = None
|
| 319 |
+
quality: str = Field(default="medium")
|
| 320 |
+
use_rag: bool = Field(default=False)
|
| 321 |
+
configuration: Optional[Dict[str, Any]] = None
|
| 322 |
+
|
| 323 |
+
class Job(BaseModel):
|
| 324 |
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 325 |
+
user_id: str # Clerk user ID
|
| 326 |
+
status: JobStatus = JobStatus.QUEUED
|
| 327 |
+
job_type: str = "video_generation"
|
| 328 |
+
configuration: Dict[str, Any]
|
| 329 |
+
progress: float = Field(default=0.0, ge=0.0, le=100.0)
|
| 330 |
+
error_message: Optional[str] = None
|
| 331 |
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
| 332 |
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
| 333 |
+
completed_at: Optional[datetime] = None
|
| 334 |
+
|
| 335 |
+
class JobResponse(BaseModel):
|
| 336 |
+
job_id: str
|
| 337 |
+
status: JobStatus
|
| 338 |
+
progress: float
|
| 339 |
+
created_at: datetime
|
| 340 |
+
estimated_completion: Optional[datetime] = None
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
**Video Model (`models/video.py`):**
|
| 344 |
+
```python
|
| 345 |
+
from pydantic import BaseModel, Field
|
| 346 |
+
from datetime import datetime
|
| 347 |
+
from typing import Optional
|
| 348 |
+
import uuid
|
| 349 |
+
|
| 350 |
+
class VideoMetadata(BaseModel):
|
| 351 |
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 352 |
+
job_id: str
|
| 353 |
+
filename: str
|
| 354 |
+
file_path: str
|
| 355 |
+
file_size: int = Field(gt=0)
|
| 356 |
+
duration: Optional[float] = Field(None, gt=0)
|
| 357 |
+
resolution: Optional[str] = None
|
| 358 |
+
format: str
|
| 359 |
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
| 360 |
+
|
| 361 |
+
class VideoResponse(BaseModel):
|
| 362 |
+
video_id: str
|
| 363 |
+
job_id: str
|
| 364 |
+
filename: str
|
| 365 |
+
file_size: int
|
| 366 |
+
duration: Optional[float]
|
| 367 |
+
download_url: str
|
| 368 |
+
created_at: datetime
|
| 369 |
+
```
|
| 370 |
+
|
| 371 |
+
#### Pydantic Schemas
|
| 372 |
+
|
| 373 |
+
**Request Schemas (`schemas/job.py`):**
|
| 374 |
+
```python
|
| 375 |
+
class VideoGenerationRequest(BaseModel):
|
| 376 |
+
topic: str = Field(..., min_length=1, max_length=500)
|
| 377 |
+
context: str = Field(..., min_length=1, max_length=2000)
|
| 378 |
+
model: Optional[str] = Field(None, description="AI model to use")
|
| 379 |
+
quality: VideoQuality = Field(VideoQuality.MEDIUM)
|
| 380 |
+
use_rag: bool = Field(False)
|
| 381 |
+
custom_config: Optional[Dict[str, Any]] = Field(None)
|
| 382 |
+
|
| 383 |
+
model_config = ConfigDict(
|
| 384 |
+
json_schema_extra={
|
| 385 |
+
"example": {
|
| 386 |
+
"topic": "Pythagorean Theorem",
|
| 387 |
+
"context": "Explain the mathematical proof with visual demonstration",
|
| 388 |
+
"model": "gemini/gemini-2.5-flash-preview-04-17",
|
| 389 |
+
"quality": "medium",
|
| 390 |
+
"use_rag": True
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
)
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
**Response Schemas:**
|
| 397 |
+
```python
|
| 398 |
+
class JobResponse(BaseModel):
|
| 399 |
+
job_id: str
|
| 400 |
+
status: JobStatus
|
| 401 |
+
created_at: datetime
|
| 402 |
+
estimated_completion: Optional[datetime] = None
|
| 403 |
+
|
| 404 |
+
class JobStatusResponse(BaseModel):
|
| 405 |
+
job_id: str
|
| 406 |
+
status: JobStatus
|
| 407 |
+
progress: float
|
| 408 |
+
current_stage: Optional[str] = None
|
| 409 |
+
error_message: Optional[str] = None
|
| 410 |
+
created_at: datetime
|
| 411 |
+
updated_at: datetime
|
| 412 |
+
completed_at: Optional[datetime] = None
|
| 413 |
+
```
|
| 414 |
+
|
| 415 |
+
## Data Models
|
| 416 |
+
|
| 417 |
+
### Core Entities
|
| 418 |
+
|
| 419 |
+
#### Job Entity
|
| 420 |
+
- **Primary Key:** UUID string
|
| 421 |
+
- **Status:** Enum (queued, processing, completed, failed, cancelled)
|
| 422 |
+
- **Configuration:** JSON field for flexible job parameters
|
| 423 |
+
- **Progress Tracking:** Float percentage and current stage
|
| 424 |
+
- **Audit Fields:** Created, updated, completed timestamps
|
| 425 |
+
- **Soft Delete:** Support for data retention policies
|
| 426 |
+
|
| 427 |
+
#### User Entity
|
| 428 |
+
- **Authentication:** JWT-based authentication
|
| 429 |
+
- **Authorization:** Role-based access control
|
| 430 |
+
- **Rate Limiting:** Per-user request limits
|
| 431 |
+
- **Audit Trail:** Request logging and monitoring
|
| 432 |
+
|
| 433 |
+
#### Video Entity
|
| 434 |
+
- **Metadata:** File size, duration, resolution, format
|
| 435 |
+
- **Storage:** File path and storage location
|
| 436 |
+
- **Relationships:** Linked to originating job
|
| 437 |
+
- **Lifecycle:** Automatic cleanup policies
|
| 438 |
+
|
| 439 |
+
### Redis Data Structure Design
|
| 440 |
+
|
| 441 |
+
#### Redis Keys and Data Types
|
| 442 |
+
|
| 443 |
+
**Job Storage (Hash):**
|
| 444 |
+
```
|
| 445 |
+
jobs:{job_id} -> Hash
|
| 446 |
+
{
|
| 447 |
+
"id": "uuid",
|
| 448 |
+
"user_id": "clerk_user_id",
|
| 449 |
+
"status": "queued|processing|completed|failed|cancelled",
|
| 450 |
+
"job_type": "video_generation",
|
| 451 |
+
"configuration": "json_string",
|
| 452 |
+
"progress": "0.0-100.0",
|
| 453 |
+
"error_message": "optional_error",
|
| 454 |
+
"created_at": "iso_datetime",
|
| 455 |
+
"updated_at": "iso_datetime",
|
| 456 |
+
"completed_at": "optional_iso_datetime"
|
| 457 |
+
}
|
| 458 |
+
```
|
| 459 |
+
|
| 460 |
+
**Job Queue (List):**
|
| 461 |
+
```
|
| 462 |
+
job_queue -> List
|
| 463 |
+
["job_id_1", "job_id_2", "job_id_3", ...]
|
| 464 |
+
```
|
| 465 |
+
|
| 466 |
+
**User Jobs Index (Set):**
|
| 467 |
+
```
|
| 468 |
+
user_jobs:{user_id} -> Set
|
| 469 |
+
{"job_id_1", "job_id_2", "job_id_3", ...}
|
| 470 |
+
```
|
| 471 |
+
|
| 472 |
+
**Video Metadata (Hash):**
|
| 473 |
+
```
|
| 474 |
+
videos:{video_id} -> Hash
|
| 475 |
+
{
|
| 476 |
+
"id": "uuid",
|
| 477 |
+
"job_id": "job_uuid",
|
| 478 |
+
"filename": "video.mp4",
|
| 479 |
+
"file_path": "/path/to/video.mp4",
|
| 480 |
+
"file_size": "bytes",
|
| 481 |
+
"duration": "seconds",
|
| 482 |
+
"resolution": "1920x1080",
|
| 483 |
+
"format": "mp4",
|
| 484 |
+
"created_at": "iso_datetime"
|
| 485 |
+
}
|
| 486 |
+
```
|
| 487 |
+
|
| 488 |
+
**Job Status Cache (String with TTL):**
|
| 489 |
+
```
|
| 490 |
+
job_status:{job_id} -> String (TTL: 300 seconds)
|
| 491 |
+
"processing" | "completed" | "failed"
|
| 492 |
+
```
|
| 493 |
+
|
| 494 |
+
**System Health (Hash):**
|
| 495 |
+
```
|
| 496 |
+
system:health -> Hash
|
| 497 |
+
{
|
| 498 |
+
"redis": "healthy",
|
| 499 |
+
"queue_length": "5",
|
| 500 |
+
"active_jobs": "3",
|
| 501 |
+
"last_check": "iso_datetime"
|
| 502 |
+
}
|
| 503 |
+
```
|
| 504 |
+
|
| 505 |
+
## Error Handling
|
| 506 |
+
|
| 507 |
+
### Exception Hierarchy
|
| 508 |
+
|
| 509 |
+
```python
|
| 510 |
+
class APIException(Exception):
|
| 511 |
+
"""Base API exception"""
|
| 512 |
+
def __init__(self, message: str, status_code: int = 500, error_code: str = None):
|
| 513 |
+
self.message = message
|
| 514 |
+
self.status_code = status_code
|
| 515 |
+
self.error_code = error_code
|
| 516 |
+
|
| 517 |
+
class ValidationException(APIException):
|
| 518 |
+
"""Request validation errors"""
|
| 519 |
+
def __init__(self, message: str, field_errors: List[dict] = None):
|
| 520 |
+
super().__init__(message, 422, "VALIDATION_ERROR")
|
| 521 |
+
self.field_errors = field_errors or []
|
| 522 |
+
|
| 523 |
+
class NotFoundException(APIException):
|
| 524 |
+
"""Resource not found"""
|
| 525 |
+
def __init__(self, resource: str):
|
| 526 |
+
super().__init__(f"{resource} not found", 404, "NOT_FOUND")
|
| 527 |
+
|
| 528 |
+
class ConflictException(APIException):
|
| 529 |
+
"""Resource conflict"""
|
| 530 |
+
def __init__(self, message: str):
|
| 531 |
+
super().__init__(message, 409, "CONFLICT")
|
| 532 |
+
|
| 533 |
+
class RateLimitException(APIException):
|
| 534 |
+
"""Rate limit exceeded"""
|
| 535 |
+
def __init__(self, retry_after: int = None):
|
| 536 |
+
super().__init__("Rate limit exceeded", 429, "RATE_LIMIT_EXCEEDED")
|
| 537 |
+
self.retry_after = retry_after
|
| 538 |
+
```
|
| 539 |
+
|
| 540 |
+
### Global Error Handler
|
| 541 |
+
|
| 542 |
+
```python
|
| 543 |
+
@app.exception_handler(APIException)
|
| 544 |
+
async def api_exception_handler(request: Request, exc: APIException):
|
| 545 |
+
return JSONResponse(
|
| 546 |
+
status_code=exc.status_code,
|
| 547 |
+
content={
|
| 548 |
+
"error": {
|
| 549 |
+
"message": exc.message,
|
| 550 |
+
"error_code": exc.error_code,
|
| 551 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 552 |
+
"path": str(request.url.path)
|
| 553 |
+
}
|
| 554 |
+
}
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
@app.exception_handler(ValidationError)
|
| 558 |
+
async def validation_exception_handler(request: Request, exc: ValidationError):
|
| 559 |
+
return JSONResponse(
|
| 560 |
+
status_code=422,
|
| 561 |
+
content={
|
| 562 |
+
"error": {
|
| 563 |
+
"message": "Validation failed",
|
| 564 |
+
"error_code": "VALIDATION_ERROR",
|
| 565 |
+
"details": exc.errors(),
|
| 566 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 567 |
+
"path": str(request.url.path)
|
| 568 |
+
}
|
| 569 |
+
}
|
| 570 |
+
)
|
| 571 |
+
```
|
| 572 |
+
|
| 573 |
+
### Error Response Format
|
| 574 |
+
|
| 575 |
+
All error responses follow a consistent structure:
|
| 576 |
+
|
| 577 |
+
```json
|
| 578 |
+
{
|
| 579 |
+
"error": {
|
| 580 |
+
"message": "Human-readable error message",
|
| 581 |
+
"error_code": "MACHINE_READABLE_CODE",
|
| 582 |
+
"details": {},
|
| 583 |
+
"timestamp": "2024-01-15T10:30:00Z",
|
| 584 |
+
"path": "/api/v1/videos/generate"
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
```
|
| 588 |
+
|
| 589 |
+
## Testing Strategy
|
| 590 |
+
|
| 591 |
+
### Testing Pyramid
|
| 592 |
+
|
| 593 |
+
#### Unit Tests (70%)
|
| 594 |
+
- **Service Layer:** Business logic validation
|
| 595 |
+
- **CRUD Operations:** Database interaction testing
|
| 596 |
+
- **Utility Functions:** Helper function validation
|
| 597 |
+
- **Schema Validation:** Pydantic model testing
|
| 598 |
+
|
| 599 |
+
#### Integration Tests (20%)
|
| 600 |
+
- **API Endpoints:** Full request/response cycle
|
| 601 |
+
- **Database Integration:** Real database operations
|
| 602 |
+
- **External Service Integration:** Mock external dependencies
|
| 603 |
+
- **WebSocket Connections:** Real-time communication testing
|
| 604 |
+
|
| 605 |
+
#### End-to-End Tests (10%)
|
| 606 |
+
- **Complete Workflows:** Full video generation pipeline
|
| 607 |
+
- **User Journeys:** Multi-step user interactions
|
| 608 |
+
- **Performance Testing:** Load and stress testing
|
| 609 |
+
- **Security Testing:** Authentication and authorization
|
| 610 |
+
|
| 611 |
+
### Test Configuration
|
| 612 |
+
|
| 613 |
+
```python
|
| 614 |
+
# conftest.py
|
| 615 |
+
@pytest.fixture
|
| 616 |
+
async def test_db():
|
| 617 |
+
"""Create test database session"""
|
| 618 |
+
engine = create_async_engine(TEST_DATABASE_URL)
|
| 619 |
+
async with engine.begin() as conn:
|
| 620 |
+
await conn.run_sync(Base.metadata.create_all)
|
| 621 |
+
|
| 622 |
+
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
| 623 |
+
async with async_session() as session:
|
| 624 |
+
yield session
|
| 625 |
+
|
| 626 |
+
async with engine.begin() as conn:
|
| 627 |
+
await conn.run_sync(Base.metadata.drop_all)
|
| 628 |
+
|
| 629 |
+
@pytest.fixture
|
| 630 |
+
def test_client():
|
| 631 |
+
"""Create test client"""
|
| 632 |
+
return TestClient(app)
|
| 633 |
+
|
| 634 |
+
@pytest.fixture
|
| 635 |
+
async def authenticated_user(test_db):
|
| 636 |
+
"""Create authenticated test user"""
|
| 637 |
+
user = await crud_users.create(
|
| 638 |
+
db=test_db,
|
| 639 |
+
object=UserCreate(
|
| 640 |
+
username="testuser",
|
| 641 |
+
email="[email protected]",
|
| 642 |
+
password="testpass123"
|
| 643 |
+
)
|
| 644 |
+
)
|
| 645 |
+
return user
|
| 646 |
+
```
|
| 647 |
+
|
| 648 |
+
### Test Examples
|
| 649 |
+
|
| 650 |
+
```python
|
| 651 |
+
# Test API endpoint
|
| 652 |
+
async def test_create_video_job(test_client, authenticated_user):
|
| 653 |
+
request_data = {
|
| 654 |
+
"topic": "Test Topic",
|
| 655 |
+
"context": "Test context for video generation",
|
| 656 |
+
"quality": "medium"
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
response = test_client.post(
|
| 660 |
+
"/api/v1/videos/generate",
|
| 661 |
+
json=request_data,
|
| 662 |
+
headers={"Authorization": f"Bearer {authenticated_user.token}"}
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
assert response.status_code == 201
|
| 666 |
+
data = response.json()
|
| 667 |
+
assert "job_id" in data
|
| 668 |
+
assert data["status"] == "queued"
|
| 669 |
+
|
| 670 |
+
# Test service layer
|
| 671 |
+
async def test_video_service_create_job(test_db):
|
| 672 |
+
service = VideoService(test_db)
|
| 673 |
+
request = VideoGenerationRequest(
|
| 674 |
+
topic="Test Topic",
|
| 675 |
+
context="Test context"
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
job = await service.create_video_job(request, user_id=1)
|
| 679 |
+
|
| 680 |
+
assert job.status == JobStatus.QUEUED
|
| 681 |
+
assert job.configuration["topic"] == "Test Topic"
|
| 682 |
+
```
|
| 683 |
+
|
| 684 |
+
## Security Considerations
|
| 685 |
+
|
| 686 |
+
### Authentication & Authorization
|
| 687 |
+
|
| 688 |
+
#### JWT Token-Based Authentication
|
| 689 |
+
- **Access Tokens:** Short-lived (15 minutes) for API access
|
| 690 |
+
- **Refresh Tokens:** Long-lived (7 days) for token renewal
|
| 691 |
+
- **Token Blacklisting:** Support for immediate token revocation
|
| 692 |
+
- **Secure Storage:** HttpOnly cookies for web clients
|
| 693 |
+
|
| 694 |
+
#### Role-Based Access Control (RBAC)
|
| 695 |
+
- **User Roles:** admin, user, readonly
|
| 696 |
+
- **Permission System:** Granular permissions for different operations
|
| 697 |
+
- **Resource Ownership:** Users can only access their own resources
|
| 698 |
+
- **Admin Override:** Administrators can access all resources
|
| 699 |
+
|
| 700 |
+
### Input Validation & Sanitization
|
| 701 |
+
|
| 702 |
+
#### Request Validation
|
| 703 |
+
- **Pydantic Models:** Automatic type validation and conversion
|
| 704 |
+
- **Field Constraints:** Length limits, format validation, range checks
|
| 705 |
+
- **Custom Validators:** Business rule validation
|
| 706 |
+
- **Sanitization:** XSS prevention and input cleaning
|
| 707 |
+
|
| 708 |
+
#### File Upload Security
|
| 709 |
+
- **File Type Validation:** Whitelist of allowed file types
|
| 710 |
+
- **Size Limits:** Maximum file size enforcement
|
| 711 |
+
- **Virus Scanning:** Integration with antivirus services
|
| 712 |
+
- **Secure Storage:** Isolated file storage with access controls
|
| 713 |
+
|
| 714 |
+
### Rate Limiting & DDoS Protection
|
| 715 |
+
|
| 716 |
+
#### Multi-Level Rate Limiting
|
| 717 |
+
- **Global Limits:** Overall API request limits
|
| 718 |
+
- **Per-User Limits:** Individual user quotas
|
| 719 |
+
- **Per-Endpoint Limits:** Specific endpoint restrictions
|
| 720 |
+
- **Sliding Window:** Advanced rate limiting algorithms
|
| 721 |
+
|
| 722 |
+
#### Implementation Strategy
|
| 723 |
+
```python
|
| 724 |
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 725 |
+
from slowapi.util import get_remote_address
|
| 726 |
+
|
| 727 |
+
limiter = Limiter(key_func=get_remote_address)
|
| 728 |
+
|
| 729 |
+
@app.route("/api/v1/videos/generate")
|
| 730 |
+
@limiter.limit("10/minute")
|
| 731 |
+
async def generate_video(request: Request):
|
| 732 |
+
# Endpoint implementation
|
| 733 |
+
pass
|
| 734 |
+
```
|
| 735 |
+
|
| 736 |
+
### Data Protection
|
| 737 |
+
|
| 738 |
+
#### Encryption
|
| 739 |
+
- **Data at Rest:** Database encryption for sensitive fields
|
| 740 |
+
- **Data in Transit:** TLS 1.3 for all communications
|
| 741 |
+
- **File Encryption:** Encrypted file storage
|
| 742 |
+
- **Key Management:** Secure key rotation policies
|
| 743 |
+
|
| 744 |
+
#### Privacy Compliance
|
| 745 |
+
- **Data Minimization:** Collect only necessary data
|
| 746 |
+
- **Retention Policies:** Automatic data cleanup
|
| 747 |
+
- **User Rights:** Data export and deletion capabilities
|
| 748 |
+
- **Audit Logging:** Comprehensive access logging
|
| 749 |
+
|
| 750 |
+
## Performance Optimization
|
| 751 |
+
|
| 752 |
+
### Caching Strategy
|
| 753 |
+
|
| 754 |
+
#### Multi-Level Caching
|
| 755 |
+
- **Application Cache:** In-memory caching with Redis
|
| 756 |
+
- **Database Query Cache:** SQLAlchemy query result caching
|
| 757 |
+
- **HTTP Response Cache:** CDN and browser caching
|
| 758 |
+
- **File System Cache:** Temporary file caching
|
| 759 |
+
|
| 760 |
+
#### Cache Implementation
|
| 761 |
+
```python
|
| 762 |
+
from fastapi_cache.decorator import cache
|
| 763 |
+
|
| 764 |
+
@router.get("/api/v1/jobs/{job_id}/status")
|
| 765 |
+
@cache(expire=30) # Cache for 30 seconds
|
| 766 |
+
async def get_job_status(job_id: str):
|
| 767 |
+
return await job_service.get_status(job_id)
|
| 768 |
+
```
|
| 769 |
+
|
| 770 |
+
### Database Optimization
|
| 771 |
+
|
| 772 |
+
#### Query Optimization
|
| 773 |
+
- **Eager Loading:** Reduce N+1 query problems
|
| 774 |
+
- **Indexing Strategy:** Optimized database indexes
|
| 775 |
+
- **Connection Pooling:** Efficient database connections
|
| 776 |
+
- **Query Monitoring:** Performance tracking and optimization
|
| 777 |
+
|
| 778 |
+
#### Pagination & Filtering
|
| 779 |
+
```python
|
| 780 |
+
async def get_jobs_paginated(
|
| 781 |
+
page: int = 1,
|
| 782 |
+
items_per_page: int = 10,
|
| 783 |
+
filters: JobFilters = None
|
| 784 |
+
) -> PaginatedResponse:
|
| 785 |
+
offset = (page - 1) * items_per_page
|
| 786 |
+
|
| 787 |
+
query = select(Job).where(Job.is_deleted == False)
|
| 788 |
+
if filters:
|
| 789 |
+
query = apply_filters(query, filters)
|
| 790 |
+
|
| 791 |
+
total = await db.scalar(select(func.count()).select_from(query.subquery()))
|
| 792 |
+
jobs = await db.execute(query.offset(offset).limit(items_per_page))
|
| 793 |
+
|
| 794 |
+
return PaginatedResponse(
|
| 795 |
+
data=jobs.scalars().all(),
|
| 796 |
+
total_count=total,
|
| 797 |
+
page=page,
|
| 798 |
+
items_per_page=items_per_page
|
| 799 |
+
)
|
| 800 |
+
```
|
| 801 |
+
|
| 802 |
+
### Asynchronous Processing
|
| 803 |
+
|
| 804 |
+
#### Background Tasks
|
| 805 |
+
- **Celery Integration:** Distributed task processing
|
| 806 |
+
- **Job Queues:** Redis-based task queuing
|
| 807 |
+
- **Progress Tracking:** Real-time progress updates
|
| 808 |
+
- **Error Recovery:** Automatic retry mechanisms
|
| 809 |
+
|
| 810 |
+
#### WebSocket Optimization
|
| 811 |
+
- **Connection Pooling:** Efficient WebSocket management
|
| 812 |
+
- **Message Broadcasting:** Efficient multi-client updates
|
| 813 |
+
- **Heartbeat Monitoring:** Connection health checks
|
| 814 |
+
- **Graceful Degradation:** Fallback to polling if needed
|
| 815 |
+
|
| 816 |
+
## Monitoring & Observability
|
| 817 |
+
|
| 818 |
+
### Logging Strategy
|
| 819 |
+
|
| 820 |
+
#### Structured Logging
|
| 821 |
+
```python
|
| 822 |
+
import structlog
|
| 823 |
+
|
| 824 |
+
logger = structlog.get_logger()
|
| 825 |
+
|
| 826 |
+
async def create_video_job(request: VideoGenerationRequest, user_id: int):
|
| 827 |
+
logger.info(
|
| 828 |
+
"Creating video job",
|
| 829 |
+
user_id=user_id,
|
| 830 |
+
topic=request.topic,
|
| 831 |
+
quality=request.quality
|
| 832 |
+
)
|
| 833 |
+
|
| 834 |
+
try:
|
| 835 |
+
job = await video_service.create_job(request, user_id)
|
| 836 |
+
logger.info("Video job created successfully", job_id=job.id)
|
| 837 |
+
return job
|
| 838 |
+
except Exception as e:
|
| 839 |
+
logger.error(
|
| 840 |
+
"Failed to create video job",
|
| 841 |
+
user_id=user_id,
|
| 842 |
+
error=str(e),
|
| 843 |
+
exc_info=True
|
| 844 |
+
)
|
| 845 |
+
raise
|
| 846 |
+
```
|
| 847 |
+
|
| 848 |
+
### Metrics Collection
|
| 849 |
+
|
| 850 |
+
#### Application Metrics
|
| 851 |
+
- **Request Metrics:** Response times, status codes, throughput
|
| 852 |
+
- **Business Metrics:** Job completion rates, user activity
|
| 853 |
+
- **System Metrics:** CPU, memory, disk usage
|
| 854 |
+
- **Custom Metrics:** Domain-specific measurements
|
| 855 |
+
|
| 856 |
+
#### Health Checks
|
| 857 |
+
```python
|
| 858 |
+
@router.get("/health")
|
| 859 |
+
async def health_check():
|
| 860 |
+
checks = {
|
| 861 |
+
"database": await check_database_health(),
|
| 862 |
+
"redis": await check_redis_health(),
|
| 863 |
+
"queue": await check_queue_health(),
|
| 864 |
+
"storage": await check_storage_health()
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
overall_health = all(checks.values())
|
| 868 |
+
status_code = 200 if overall_health else 503
|
| 869 |
+
|
| 870 |
+
return JSONResponse(
|
| 871 |
+
status_code=status_code,
|
| 872 |
+
content={
|
| 873 |
+
"status": "healthy" if overall_health else "unhealthy",
|
| 874 |
+
"checks": checks,
|
| 875 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 876 |
+
}
|
| 877 |
+
)
|
| 878 |
+
```
|
| 879 |
+
|
| 880 |
+
### Distributed Tracing
|
| 881 |
+
|
| 882 |
+
#### OpenTelemetry Integration
|
| 883 |
+
- **Request Tracing:** End-to-end request tracking
|
| 884 |
+
- **Service Dependencies:** Inter-service communication mapping
|
| 885 |
+
- **Performance Analysis:** Bottleneck identification
|
| 886 |
+
- **Error Correlation:** Error tracking across services
|
| 887 |
+
|
| 888 |
+
## Deployment Architecture
|
| 889 |
+
|
| 890 |
+
### Container Strategy
|
| 891 |
+
|
| 892 |
+
#### Docker Configuration
|
| 893 |
+
```dockerfile
|
| 894 |
+
FROM python:3.11-slim
|
| 895 |
+
|
| 896 |
+
WORKDIR /app
|
| 897 |
+
|
| 898 |
+
# Install system dependencies
|
| 899 |
+
RUN apt-get update && apt-get install -y \
|
| 900 |
+
gcc \
|
| 901 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 902 |
+
|
| 903 |
+
# Install Python dependencies
|
| 904 |
+
COPY requirements.txt .
|
| 905 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 906 |
+
|
| 907 |
+
# Copy application code
|
| 908 |
+
COPY src/ ./src/
|
| 909 |
+
COPY migrations/ ./migrations/
|
| 910 |
+
|
| 911 |
+
# Set environment variables
|
| 912 |
+
ENV PYTHONPATH=/app/src
|
| 913 |
+
ENV PYTHONUNBUFFERED=1
|
| 914 |
+
|
| 915 |
+
# Expose port
|
| 916 |
+
EXPOSE 8000
|
| 917 |
+
|
| 918 |
+
# Run application
|
| 919 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
| 920 |
+
```
|
| 921 |
+
|
| 922 |
+
#### AWS Deployment Configuration
|
| 923 |
+
|
| 924 |
+
**AWS Services Integration:**
|
| 925 |
+
- **RDS PostgreSQL:** Primary relational database for user data, jobs, subscriptions
|
| 926 |
+
- **DynamoDB:** High-frequency data like job status updates, user activity tracking
|
| 927 |
+
- **S3:** Video files, thumbnails, job outputs, user uploads
|
| 928 |
+
- **ElastiCache Redis:** Session storage, caching, real-time data
|
| 929 |
+
- **SQS:** Job queue management and inter-service communication
|
| 930 |
+
- **CloudFront:** CDN for video delivery and static assets
|
| 931 |
+
- **Lambda:** Serverless functions for background processing
|
| 932 |
+
- **ECS/Fargate:** Container orchestration for API services
|
| 933 |
+
|
| 934 |
+
**Docker Compose for Local Development:**
|
| 935 |
+
```yaml
|
| 936 |
+
version: '3.8'
|
| 937 |
+
|
| 938 |
+
services:
|
| 939 |
+
api:
|
| 940 |
+
build: .
|
| 941 |
+
ports:
|
| 942 |
+
- "8000:8000"
|
| 943 |
+
environment:
|
| 944 |
+
- AWS_REGION=us-east-1
|
| 945 |
+
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
| 946 |
+
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
| 947 |
+
- DATABASE_URL=${RDS_DATABASE_URL}
|
| 948 |
+
- REDIS_URL=${ELASTICACHE_URL}
|
| 949 |
+
- S3_BUCKET=${S3_BUCKET_NAME}
|
| 950 |
+
- SQS_QUEUE_URL=${SQS_QUEUE_URL}
|
| 951 |
+
- DYNAMODB_TABLE_PREFIX=${DYNAMODB_PREFIX}
|
| 952 |
+
volumes:
|
| 953 |
+
- ./logs:/app/logs
|
| 954 |
+
- ~/.aws:/root/.aws:ro
|
| 955 |
+
|
| 956 |
+
localstack:
|
| 957 |
+
image: localstack/localstack:latest
|
| 958 |
+
ports:
|
| 959 |
+
- "4566:4566"
|
| 960 |
+
environment:
|
| 961 |
+
- SERVICES=s3,sqs,dynamodb,elasticache
|
| 962 |
+
- DEBUG=1
|
| 963 |
+
- DATA_DIR=/tmp/localstack/data
|
| 964 |
+
volumes:
|
| 965 |
+
- localstack_data:/tmp/localstack
|
| 966 |
+
|
| 967 |
+
postgres:
|
| 968 |
+
image: postgres:15
|
| 969 |
+
environment:
|
| 970 |
+
- POSTGRES_DB=videoapi_local
|
| 971 |
+
- POSTGRES_USER=user
|
| 972 |
+
- POSTGRES_PASSWORD=pass
|
| 973 |
+
volumes:
|
| 974 |
+
- postgres_data:/var/lib/postgresql/data
|
| 975 |
+
ports:
|
| 976 |
+
- "5432:5432"
|
| 977 |
+
|
| 978 |
+
volumes:
|
| 979 |
+
postgres_data:
|
| 980 |
+
localstack_data:
|
| 981 |
+
```
|
| 982 |
+
|
| 983 |
+
**AWS CDK/Terraform Infrastructure:**
|
| 984 |
+
```typescript
|
| 985 |
+
// AWS CDK example structure
|
| 986 |
+
export class VideoAPIStack extends Stack {
|
| 987 |
+
constructor(scope: Construct, id: string, props?: StackProps) {
|
| 988 |
+
super(scope, id, props);
|
| 989 |
+
|
| 990 |
+
// RDS PostgreSQL
|
| 991 |
+
const database = new rds.DatabaseInstance(this, 'VideoAPIDB', {
|
| 992 |
+
engine: rds.DatabaseInstanceEngine.postgres({
|
| 993 |
+
version: rds.PostgresEngineVersion.VER_15
|
| 994 |
+
}),
|
| 995 |
+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
|
| 996 |
+
multiAz: true,
|
| 997 |
+
backupRetention: Duration.days(7)
|
| 998 |
+
});
|
| 999 |
+
|
| 1000 |
+
// S3 Buckets
|
| 1001 |
+
const videoBucket = new s3.Bucket(this, 'VideoBucket', {
|
| 1002 |
+
versioned: true,
|
| 1003 |
+
lifecycleRules: [{
|
| 1004 |
+
id: 'DeleteOldVersions',
|
| 1005 |
+
expiration: Duration.days(90)
|
| 1006 |
+
}]
|
| 1007 |
+
});
|
| 1008 |
+
|
| 1009 |
+
// DynamoDB Tables
|
| 1010 |
+
const jobStatusTable = new dynamodb.Table(this, 'JobStatusTable', {
|
| 1011 |
+
partitionKey: { name: 'job_id', type: dynamodb.AttributeType.STRING },
|
| 1012 |
+
sortKey: { name: 'timestamp', type: dynamodb.AttributeType.NUMBER },
|
| 1013 |
+
timeToLiveAttribute: 'ttl'
|
| 1014 |
+
});
|
| 1015 |
+
|
| 1016 |
+
// ECS Fargate Service
|
| 1017 |
+
const cluster = new ecs.Cluster(this, 'VideoAPICluster');
|
| 1018 |
+
const taskDefinition = new ecs.FargateTaskDefinition(this, 'VideoAPITask');
|
| 1019 |
+
|
| 1020 |
+
const container = taskDefinition.addContainer('api', {
|
| 1021 |
+
image: ecs.ContainerImage.fromRegistry('your-api-image'),
|
| 1022 |
+
environment: {
|
| 1023 |
+
DATABASE_URL: database.instanceEndpoint.socketAddress,
|
| 1024 |
+
S3_BUCKET: videoBucket.bucketName
|
| 1025 |
+
}
|
| 1026 |
+
});
|
| 1027 |
+
|
| 1028 |
+
new ecs.FargateService(this, 'VideoAPIService', {
|
| 1029 |
+
cluster,
|
| 1030 |
+
taskDefinition,
|
| 1031 |
+
desiredCount: 2
|
| 1032 |
+
});
|
| 1033 |
+
}
|
| 1034 |
+
}
|
| 1035 |
+
```
|
| 1036 |
+
|
| 1037 |
+
### Production Considerations
|
| 1038 |
+
|
| 1039 |
+
#### Scalability
|
| 1040 |
+
- **Horizontal Scaling:** Multiple API instances behind load balancer
|
| 1041 |
+
- **Database Scaling:** Read replicas and connection pooling
|
| 1042 |
+
- **Cache Scaling:** Redis clustering for high availability
|
| 1043 |
+
- **File Storage:** Distributed storage solutions
|
| 1044 |
+
|
| 1045 |
+
#### Security Hardening
|
| 1046 |
+
- **SSL/TLS:** End-to-end encryption
|
| 1047 |
+
- **Firewall Rules:** Network access restrictions
|
| 1048 |
+
- **Secret Management:** Secure credential storage
|
| 1049 |
+
- **Regular Updates:** Security patch management
|
| 1050 |
+
|
| 1051 |
+
#### Monitoring & Alerting
|
| 1052 |
+
- **Application Monitoring:** APM tools integration
|
| 1053 |
+
- **Infrastructure Monitoring:** System metrics collection
|
| 1054 |
+
- **Log Aggregation:** Centralized logging solution
|
| 1055 |
+
- **Alert Management:** Proactive issue notification
|
.kiro/specs/fastapi-backend/requirements.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
This document outlines the requirements for implementing a simple FastAPI backend for the multi-agent video generation system. The backend will use Pydantic for all data modeling and validation, Clerk for authentication, and Redis for caching and job queuing. The system will provide RESTful API endpoints to manage video generation requests, monitor processing status, and integrate with the existing multi-agent pipeline through Redis queues.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As a client application, I want to submit video generation requests through a REST API, so that I can programmatically create educational videos from textual descriptions.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN a POST request is made to `/api/v1/videos/generate` with topic and context data THEN the system SHALL accept the request and return a unique job ID
|
| 16 |
+
2. WHEN the request includes optional parameters like model selection, quality settings, or RAG configuration THEN the system SHALL validate and apply these parameters
|
| 17 |
+
3. WHEN the request payload is invalid or missing required fields THEN the system SHALL return a 422 validation error with detailed field-level error messages
|
| 18 |
+
4. WHEN the system receives a valid request THEN it SHALL queue the job for processing and return a 201 status code with job metadata
|
| 19 |
+
|
| 20 |
+
### Requirement 2
|
| 21 |
+
|
| 22 |
+
**User Story:** As a client application, I want to monitor the status of video generation jobs, so that I can track progress and know when videos are ready for download.
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN a GET request is made to `/api/v1/videos/jobs/{job_id}/status` THEN the system SHALL return the current job status (queued, processing, completed, failed)
|
| 27 |
+
2. WHEN a job is in progress THEN the system SHALL return progress information including current stage and percentage completion
|
| 28 |
+
3. WHEN a job has failed THEN the system SHALL return error details and failure reason
|
| 29 |
+
4. WHEN a job is completed THEN the system SHALL return metadata about the generated video including file size and duration
|
| 30 |
+
5. WHEN an invalid job ID is provided THEN the system SHALL return a 404 error
|
| 31 |
+
|
| 32 |
+
### Requirement 3
|
| 33 |
+
|
| 34 |
+
**User Story:** As a client application, I want to retrieve completed videos and their metadata, so that I can download and use the generated content.
|
| 35 |
+
|
| 36 |
+
#### Acceptance Criteria
|
| 37 |
+
|
| 38 |
+
1. WHEN a GET request is made to `/api/v1/videos/jobs/{job_id}/download` for a completed job THEN the system SHALL return the video file as a streaming response
|
| 39 |
+
2. WHEN a GET request is made to `/api/v1/videos/jobs/{job_id}/metadata` THEN the system SHALL return comprehensive job metadata including processing logs and performance metrics
|
| 40 |
+
3. WHEN a job is not yet completed THEN the download endpoint SHALL return a 409 conflict error
|
| 41 |
+
4. WHEN the video file is not found THEN the system SHALL return a 404 error
|
| 42 |
+
5. WHEN downloading large files THEN the system SHALL support HTTP range requests for partial content delivery
|
| 43 |
+
|
| 44 |
+
### Requirement 4
|
| 45 |
+
|
| 46 |
+
**User Story:** As a system administrator, I want to manage and monitor the video generation pipeline, so that I can ensure optimal system performance and troubleshoot issues.
|
| 47 |
+
|
| 48 |
+
#### Acceptance Criteria
|
| 49 |
+
|
| 50 |
+
1. WHEN a GET request is made to `/api/v1/system/health` THEN the system SHALL return health status of all components including database, queue, and agent services
|
| 51 |
+
2. WHEN a GET request is made to `/api/v1/system/metrics` THEN the system SHALL return performance metrics including queue length, processing times, and resource utilization
|
| 52 |
+
3. WHEN a POST request is made to `/api/v1/system/jobs/{job_id}/cancel` THEN the system SHALL attempt to cancel the job and return cancellation status
|
| 53 |
+
4. WHEN a GET request is made to `/api/v1/system/jobs` with pagination parameters THEN the system SHALL return a paginated list of all jobs with filtering options
|
| 54 |
+
5. WHEN system resources are critically low THEN the health endpoint SHALL return a 503 service unavailable status
|
| 55 |
+
|
| 56 |
+
### Requirement 5
|
| 57 |
+
|
| 58 |
+
**User Story:** As a client application, I want to upload custom content and configurations, so that I can customize the video generation process with specific materials or settings.
|
| 59 |
+
|
| 60 |
+
#### Acceptance Criteria
|
| 61 |
+
|
| 62 |
+
1. WHEN a POST request is made to `/api/v1/uploads/content` with multipart form data THEN the system SHALL accept and store uploaded files securely
|
| 63 |
+
2. WHEN uploading files THEN the system SHALL validate file types, sizes, and scan for malicious content
|
| 64 |
+
3. WHEN a POST request is made to `/api/v1/configurations` with custom settings THEN the system SHALL validate and store the configuration for later use
|
| 65 |
+
4. WHEN uploaded content exceeds size limits THEN the system SHALL return a 413 payload too large error
|
| 66 |
+
5. WHEN invalid file types are uploaded THEN the system SHALL return a 415 unsupported media type error
|
| 67 |
+
|
| 68 |
+
### Requirement 6
|
| 69 |
+
|
| 70 |
+
**User Story:** As a client application, I want to authenticate requests using Clerk authentication, so that the system remains secure and user access is properly managed.
|
| 71 |
+
|
| 72 |
+
#### Acceptance Criteria
|
| 73 |
+
|
| 74 |
+
1. WHEN a request is made without a valid Clerk session token THEN the system SHALL return a 401 unauthorized error
|
| 75 |
+
2. WHEN a request is made with an invalid or expired Clerk token THEN the system SHALL return a 401 unauthorized error
|
| 76 |
+
3. WHEN a valid Clerk token is provided THEN the system SHALL extract user information and process the request
|
| 77 |
+
4. WHEN user information is needed THEN the system SHALL retrieve it from Clerk's user management system
|
| 78 |
+
5. WHEN rate limits are exceeded THEN the system SHALL return a 429 too many requests error
|
| 79 |
+
|
| 80 |
+
### Requirement 7
|
| 81 |
+
|
| 82 |
+
**User Story:** As a developer integrating with the API, I want comprehensive API documentation and client generation capabilities, so that I can efficiently build applications that consume the video generation service.
|
| 83 |
+
|
| 84 |
+
#### Acceptance Criteria
|
| 85 |
+
|
| 86 |
+
1. WHEN accessing `/docs` THEN the system SHALL provide interactive Swagger UI documentation with all endpoints and schemas
|
| 87 |
+
2. WHEN accessing `/redoc` THEN the system SHALL provide ReDoc documentation interface
|
| 88 |
+
3. WHEN accessing `/openapi.json` THEN the system SHALL return the complete OpenAPI specification
|
| 89 |
+
4. WHEN generating client code THEN the OpenAPI specification SHALL include proper operation IDs and detailed schemas
|
| 90 |
+
5. WHEN API changes are made THEN the documentation SHALL automatically update to reflect the current API state
|
| 91 |
+
|
| 92 |
+
### Requirement 8
|
| 93 |
+
|
| 94 |
+
**User Story:** As a system operator, I want the API to handle errors gracefully and provide detailed logging, so that I can maintain system reliability and troubleshoot issues effectively.
|
| 95 |
+
|
| 96 |
+
#### Acceptance Criteria
|
| 97 |
+
|
| 98 |
+
1. WHEN any error occurs THEN the system SHALL return appropriate HTTP status codes with consistent error response format
|
| 99 |
+
2. WHEN internal errors occur THEN the system SHALL log detailed error information without exposing sensitive data to clients
|
| 100 |
+
3. WHEN validation errors occur THEN the system SHALL return specific field-level error messages
|
| 101 |
+
4. WHEN the system is under high load THEN it SHALL implement proper backpressure and queue management
|
| 102 |
+
5. WHEN critical errors occur THEN the system SHALL trigger appropriate alerting mechanisms
|
| 103 |
+
|
| 104 |
+
### Requirement 9
|
| 105 |
+
|
| 106 |
+
**User Story:** As a client application, I want to receive real-time updates about job progress, so that I can provide live feedback to users about video generation status.
|
| 107 |
+
|
| 108 |
+
#### Acceptance Criteria
|
| 109 |
+
|
| 110 |
+
1. WHEN a WebSocket connection is established to `/ws/jobs/{job_id}` THEN the system SHALL provide real-time status updates
|
| 111 |
+
2. WHEN job status changes THEN connected WebSocket clients SHALL receive immediate notifications
|
| 112 |
+
3. WHEN processing stages complete THEN clients SHALL receive detailed progress information
|
| 113 |
+
4. WHEN WebSocket connections are lost THEN the system SHALL handle reconnection gracefully
|
| 114 |
+
5. WHEN multiple clients connect to the same job THEN all SHALL receive synchronized updates
|
| 115 |
+
|
| 116 |
+
### Requirement 10
|
| 117 |
+
|
| 118 |
+
**User Story:** As a system integrator, I want the API to support batch operations and bulk processing, so that I can efficiently handle multiple video generation requests simultaneously.
|
| 119 |
+
|
| 120 |
+
#### Acceptance Criteria
|
| 121 |
+
|
| 122 |
+
1. WHEN a POST request is made to `/api/v1/videos/batch` with multiple video requests THEN the system SHALL create multiple jobs and return batch job metadata
|
| 123 |
+
2. WHEN batch processing is requested THEN the system SHALL optimize resource allocation across multiple jobs
|
| 124 |
+
3. WHEN batch jobs are queried THEN the system SHALL return aggregated status information for all jobs in the batch
|
| 125 |
+
4. WHEN individual jobs in a batch fail THEN other jobs SHALL continue processing independently
|
| 126 |
+
5. WHEN batch size exceeds system limits THEN the system SHALL return appropriate error messages with suggested batch sizes
|
.kiro/specs/fastapi-backend/tasks.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [ ] 1. Set up project structure and basic configuration
|
| 4 |
+
|
| 5 |
+
- Create FastAPI project directory structure following the simplified design specification
|
| 6 |
+
- Set up pyproject.toml with required dependencies (FastAPI, Pydantic, Redis, Clerk SDK)
|
| 7 |
+
- Create main.py with basic FastAPI application initialization
|
| 8 |
+
- Configure environment-based settings using Pydantic BaseSettings
|
| 9 |
+
- Set up basic logging configuration
|
| 10 |
+
- _Requirements: 1.1, 6.4, 7.5_
|
| 11 |
+
|
| 12 |
+
- [x] 2. Implement Redis infrastructure and connection
|
| 13 |
+
|
| 14 |
+
- [x] 2.1 Set up Redis connection and utilities
|
| 15 |
+
|
| 16 |
+
- Create redis.py with Redis connection management
|
| 17 |
+
- Implement Redis dependency injection for FastAPI endpoints
|
| 18 |
+
- Configure Redis connection pooling and error handling
|
| 19 |
+
- Add Redis health check utilities
|
| 20 |
+
- _Requirements: 1.1, 2.1, 4.1_
|
| 21 |
+
|
| 22 |
+
- [x] 2.2 Create Redis data access patterns
|
| 23 |
+
|
| 24 |
+
- Implement Redis hash operations for job storage
|
| 25 |
+
- Create Redis list operations for job queue management
|
| 26 |
+
- Add Redis set operations for user job indexing
|
| 27 |
+
- Implement Redis key expiration and cleanup utilities
|
| 28 |
+
- _Requirements: 1.1, 2.1, 8.2_
|
| 29 |
+
|
| 30 |
+
- [x] 3. Implement Clerk authentication integration
|
| 31 |
+
|
| 32 |
+
- [x] 3.1 Set up Clerk authentication middleware
|
| 33 |
+
|
| 34 |
+
- Create Clerk SDK integration and configuration
|
| 35 |
+
- Implement Clerk token validation middleware
|
| 36 |
+
- Build authentication dependency for protected endpoints
|
| 37 |
+
- Add user information extraction from Clerk tokens
|
| 38 |
+
- _Requirements: 6.1, 6.2, 6.3_
|
| 39 |
+
|
| 40 |
+
- [x] 3.2 Create user management utilities
|
| 41 |
+
|
| 42 |
+
- Implement user data extraction from Clerk
|
| 43 |
+
- Create user session management utilities
|
| 44 |
+
- Add user permission checking functions
|
| 45 |
+
- _Requirements: 6.1, 6.2, 8.1_
|
| 46 |
+
|
| 47 |
+
- [ ] 4. Create Pydantic data models
|
| 48 |
+
|
| 49 |
+
- [x] 4.1 Define core Pydantic models
|
| 50 |
+
|
| 51 |
+
- Create Job model with validation rules and status enum
|
| 52 |
+
- Implement VideoMetadata model for video information
|
| 53 |
+
- Define User model for Clerk user data
|
| 54 |
+
- Create SystemHealth model for monitoring
|
| 55 |
+
- Add common response models and error schemas
|
| 56 |
+
- _Requirements: 1.1, 1.3, 2.1, 7.3, 8.3_
|
| 57 |
+
|
| 58 |
+
- [x] 4.2 Implement request/response schemas
|
| 59 |
+
|
| 60 |
+
- Create VideoGenerationRequest schema with field validation
|
| 61 |
+
- Define JobResponse and JobStatusResponse schemas
|
| 62 |
+
- Implement pagination and filtering schemas
|
| 63 |
+
- Add error response schemas with consistent structure
|
| 64 |
+
- Create API documentation examples for all schemas
|
| 65 |
+
- _Requirements: 1.1, 1.3, 2.1, 7.3, 8.3_
|
| 66 |
+
|
| 67 |
+
- [x] 5. Build core API endpoints
|
| 68 |
+
|
| 69 |
+
- [x] 5.1 Implement video generation endpoints
|
| 70 |
+
|
| 71 |
+
- Create POST /api/v1/videos/generate endpoint with Pydantic validation
|
| 72 |
+
- Implement GET /api/v1/videos/jobs/{job_id}/status endpoint with Redis data
|
| 73 |
+
- Build GET /api/v1/videos/jobs/{job_id}/download endpoint for file serving
|
| 74 |
+
- Add GET /api/v1/videos/jobs/{job_id}/metadata endpoint
|
| 75 |
+
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2_
|
| 76 |
+
|
| 77 |
+
- [x] 5.2 Implement job management endpoints
|
| 78 |
+
|
| 79 |
+
- Create GET /api/v1/jobs endpoint with pagination and filtering
|
| 80 |
+
- Implement POST /api/v1/jobs/{job_id}/cancel endpoint
|
| 81 |
+
- Build DELETE /api/v1/jobs/{job_id} endpoint with Redis cleanup
|
| 82 |
+
- Add GET /api/v1/jobs/{job_id}/logs endpoint
|
| 83 |
+
- _Requirements: 2.1, 2.2, 4.3, 10.3_
|
| 84 |
+
|
| 85 |
+
- [x] 5.3 Create system monitoring endpoints
|
| 86 |
+
|
| 87 |
+
- Implement GET /api/v1/system/health endpoint with Redis and queue checks
|
| 88 |
+
- Create GET /api/v1/system/metrics endpoint for basic system stats
|
| 89 |
+
- Add GET /api/v1/system/queue-status endpoint for queue monitoring
|
| 90 |
+
- _Requirements: 4.1, 4.2, 4.5_
|
| 91 |
+
|
| 92 |
+
- [x] 6. Implement business logic services
|
| 93 |
+
|
| 94 |
+
- [x] 6.1 Create video generation service
|
| 95 |
+
|
| 96 |
+
- Implement VideoService class with Redis job queue integration
|
| 97 |
+
- Add job creation and status management methods
|
| 98 |
+
- Create progress tracking and update mechanisms
|
| 99 |
+
- Implement job queue processing logic
|
| 100 |
+
- _Requirements: 1.1, 1.2, 2.1, 2.2, 9.2_
|
| 101 |
+
|
| 102 |
+
- [x] 6.2 Build job management service
|
| 103 |
+
|
| 104 |
+
- Implement JobService with Redis-based storage
|
| 105 |
+
- Add job lifecycle management methods
|
| 106 |
+
- Create job cancellation and cleanup functionality
|
| 107 |
+
- Implement job metrics collection
|
| 108 |
+
- _Requirements: 2.1, 2.2, 4.3, 10.1, 10.2, 10.4_
|
| 109 |
+
|
| 110 |
+
- [x] 6.3 Create queue management service
|
| 111 |
+
|
| 112 |
+
- Implement QueueService for Redis queue operations
|
| 113 |
+
|
| 114 |
+
- Add job queuing and dequeuing methods
|
| 115 |
+
- Create queue monitoring and health check functions
|
| 116 |
+
- Implement queue cleanup and maintenance utilities
|
| 117 |
+
- _Requirements: 1.1, 2.1, 4.2_
|
| 118 |
+
|
| 119 |
+
- [x] 7. Add file handling and storage
|
| 120 |
+
|
| 121 |
+
- [x] 7.1 Implement local file management
|
| 122 |
+
|
| 123 |
+
- Create file upload handling with validation
|
| 124 |
+
- Implement secure file storage with proper permissions
|
| 125 |
+
- Add file metadata extraction and storage in Redis
|
| 126 |
+
- Create file cleanup and maintenance utilities
|
| 127 |
+
- _Requirements: 3.1, 3.2, 3.3, 5.1, 5.2, 5.4, 5.5_
|
| 128 |
+
|
| 129 |
+
- [x] 7.2 Build file serving capabilities
|
| 130 |
+
|
| 131 |
+
- Implement file download endpoints with streaming
|
| 132 |
+
- Add file access control and security checks
|
| 133 |
+
- Create file URL generation for frontend access
|
| 134 |
+
- Implement file caching strategies
|
| 135 |
+
- _Requirements: 3.1, 3.2, 3.3_
|
| 136 |
+
|
| 137 |
+
- [ ] 8. Implement error handling and middleware
|
| 138 |
+
|
| 139 |
+
- [x] 8.1 Create global exception handling
|
| 140 |
+
|
| 141 |
+
- Implement custom exception classes with proper HTTP status codes
|
| 142 |
+
- Create global exception handler middleware
|
| 143 |
+
- Add structured error response formatting
|
| 144 |
+
- Implement error logging with request correlation
|
| 145 |
+
- _Requirements: 8.1, 8.2, 8.3_
|
| 146 |
+
|
| 147 |
+
- [x] 8.2 Build request/response middleware
|
| 148 |
+
|
| 149 |
+
- Implement CORS middleware for cross-origin requests
|
| 150 |
+
- Create request logging middleware with performance metrics
|
| 151 |
+
- Add response compression middleware
|
| 152 |
+
- Implement security headers middleware
|
| 153 |
+
- _Requirements: 8.2, 8.4_
|
| 154 |
+
|
| 155 |
+
- [x] 9. Add caching and performance optimization
|
| 156 |
+
|
| 157 |
+
- [x] 9.1 Implement Redis caching strategies
|
| 158 |
+
|
| 159 |
+
- Create cache decorator for frequently accessed endpoints
|
| 160 |
+
- Implement cache invalidation patterns
|
| 161 |
+
|
| 162 |
+
- Add cache warming strategies for common queries
|
| 163 |
+
- Create cache monitoring and metrics collection
|
| 164 |
+
- _Requirements: 2.1, 4.2_
|
| 165 |
+
|
| 166 |
+
- [x] 9.2 Optimize API performance
|
| 167 |
+
|
| 168 |
+
- Implement response caching for static data
|
| 169 |
+
- Add request deduplication for expensive operations
|
| 170 |
+
- Create connection pooling optimization
|
| 171 |
+
- Implement async processing where beneficial
|
| 172 |
+
- _Requirements: 2.1, 4.2, 10.3_
|
| 173 |
+
|
| 174 |
+
- [ ] 10. Implement batch processing capabilities
|
| 175 |
+
|
| 176 |
+
- [ ] 10.1 Create batch job endpoints
|
| 177 |
+
|
| 178 |
+
- Implement POST /api/v1/videos/batch endpoint
|
| 179 |
+
- Add batch job validation and processing logic
|
| 180 |
+
- Create batch status tracking and reporting
|
| 181 |
+
- _Requirements: 10.1, 10.2_
|
| 182 |
+
|
| 183 |
+
- [ ] 10.2 Build batch job management
|
| 184 |
+
- Implement batch job cancellation and cleanup
|
| 185 |
+
- Add batch job progress aggregation
|
| 186 |
+
- Create batch job completion notifications
|
| 187 |
+
- _Requirements: 10.3, 10.4, 10.5_
|
| 188 |
+
|
| 189 |
+
- [ ] 11. Add comprehensive testing suite
|
| 190 |
+
|
| 191 |
+
- [ ] 11.1 Create unit tests for core functionality
|
| 192 |
+
|
| 193 |
+
- Write unit tests for all service layer methods
|
| 194 |
+
- Create tests for Redis operations and data models
|
| 195 |
+
- Add tests for Clerk authentication integration
|
| 196 |
+
- Test Pydantic schema validation and serialization
|
| 197 |
+
- _Requirements: 1.1, 1.3, 2.1, 6.1, 8.3_
|
| 198 |
+
|
| 199 |
+
- [ ] 11.2 Implement integration tests for API endpoints
|
| 200 |
+
|
| 201 |
+
- Create integration tests for all video generation endpoints
|
| 202 |
+
- Test job management endpoints with Redis operations
|
| 203 |
+
- Add file upload and download functionality tests
|
| 204 |
+
- Test error handling and edge cases
|
| 205 |
+
- _Requirements: 1.1, 2.1, 3.1, 9.1_
|
| 206 |
+
|
| 207 |
+
- [ ] 11.3 Build end-to-end workflow tests
|
| 208 |
+
- Create complete video generation workflow tests
|
| 209 |
+
- Test batch processing end-to-end scenarios
|
| 210 |
+
- Add performance testing for critical endpoints
|
| 211 |
+
- Test system recovery and error scenarios
|
| 212 |
+
- _Requirements: 1.1, 8.1, 10.1_
|
| 213 |
+
|
| 214 |
+
- [ ] 12. Implement rate limiting and security features
|
| 215 |
+
|
| 216 |
+
- [ ] 12.1 Add rate limiting middleware
|
| 217 |
+
|
| 218 |
+
- Implement Redis-based rate limiting per user and endpoint
|
| 219 |
+
- Create rate limit configuration and storage
|
| 220 |
+
- Add rate limit headers and error responses
|
| 221 |
+
- Implement rate limit monitoring and alerting
|
| 222 |
+
- _Requirements: 6.5, 8.4_
|
| 223 |
+
|
| 224 |
+
- [ ] 12.2 Enhance security measures
|
| 225 |
+
- Implement input sanitization and validation
|
| 226 |
+
- Add request/response logging for audit trails
|
| 227 |
+
- Create security headers middleware
|
| 228 |
+
- Implement API key validation for internal services
|
| 229 |
+
- _Requirements: 6.1, 6.3, 8.2_
|
| 230 |
+
|
| 231 |
+
- [x] 13. Create API documentation and client generation
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
- [x] 13.1 Configure OpenAPI documentation
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
- Customize OpenAPI schema generation with proper operation IDs
|
| 242 |
+
- Add comprehensive endpoint descriptions and examples
|
| 243 |
+
- Configure Swagger UI and ReDoc interfaces
|
| 244 |
+
- Add authentication documentation for Clerk integration
|
| 245 |
+
- _Requirements: 7.1, 7.2, 7.3_
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
- [ ] 13.2 Set up client code generation
|
| 250 |
+
- Configure OpenAPI specification for client generation
|
| 251 |
+
- Create example client generation scripts
|
| 252 |
+
- Add client SDK documentation and examples
|
| 253 |
+
- Test generated clients with real API endpoints
|
| 254 |
+
- _Requirements: 7.4, 7.5_
|
| 255 |
+
|
| 256 |
+
- [ ] 14. Implement deployment configuration
|
| 257 |
+
|
| 258 |
+
- [ ] 14.1 Create Docker containerization
|
| 259 |
+
|
| 260 |
+
- Write Dockerfile with multi-stage build optimization
|
| 261 |
+
- Create docker-compose.yml for local development with Redis
|
| 262 |
+
- Add production docker-compose with proper networking
|
| 263 |
+
- Configure environment variable management
|
| 264 |
+
- _Requirements: 4.1, 8.4_
|
| 265 |
+
|
| 266 |
+
- [ ] 14.2 Add monitoring and logging
|
| 267 |
+
- Implement structured logging with JSON format
|
| 268 |
+
- Add application metrics collection and export
|
| 269 |
+
- Create health check endpoints for container orchestration
|
| 270 |
+
- Configure log aggregation and monitoring dashboards
|
| 271 |
+
- _Requirements: 4.1, 4.2, 8.2_
|
| 272 |
+
|
| 273 |
+
- [ ] 15. Final integration and optimization
|
| 274 |
+
|
| 275 |
+
- [ ] 15.1 Integrate with existing video generation pipeline
|
| 276 |
+
|
| 277 |
+
- Create Redis queue integration with multi-agent video generation system
|
| 278 |
+
- Implement job queue management with proper error handling
|
| 279 |
+
- Add configuration mapping between API requests and pipeline parameters
|
| 280 |
+
- Test end-to-end video generation workflow
|
| 281 |
+
- _Requirements: 1.1, 1.2, 2.1_
|
| 282 |
+
|
| 283 |
+
- [ ] 15.2 Performance optimization and cleanup
|
| 284 |
+
- Optimize Redis operations and connection management
|
| 285 |
+
- Implement proper resource cleanup and garbage collection
|
| 286 |
+
- Add graceful shutdown handling for long-running operations
|
| 287 |
+
- Create production-ready configuration templates
|
| 288 |
+
- _Requirements: 4.2, 8.4_
|
| 289 |
+
|
| 290 |
+
- [ ] 16. Frontend integration preparation
|
| 291 |
+
|
| 292 |
+
- [ ] 16.1 Create frontend-specific endpoints
|
| 293 |
+
|
| 294 |
+
- Implement GET /api/v1/dashboard/stats endpoint for user dashboard
|
| 295 |
+
- Create GET /api/v1/dashboard/recent-jobs endpoint
|
| 296 |
+
- Add WebSocket endpoints for real-time job updates
|
| 297 |
+
- Implement user preference and settings endpoints
|
| 298 |
+
- _Requirements: 2.1, 9.1, 9.2_
|
| 299 |
+
|
| 300 |
+
- [ ] 16.2 Set up CORS and frontend security
|
| 301 |
+
- Configure CORS middleware for frontend domain access
|
| 302 |
+
- Add rate limiting specific to frontend endpoints
|
| 303 |
+
- Create frontend-specific authentication flows with Clerk
|
| 304 |
+
- Implement proper session management for frontend clients
|
| 305 |
+
- _Requirements: 6.1, 6.5, 8.4_
|
API_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,1311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# T2M API Documentation
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This document provides comprehensive documentation for the T2M (Text-to-Media) API endpoints. The API is organized into several modules: Authentication, Files, Jobs, System, and Videos.
|
| 6 |
+
|
| 7 |
+
## Base URL
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
https://your-api-domain.com/api/v1
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## Authentication
|
| 14 |
+
|
| 15 |
+
Most endpoints require authentication. Include the authorization token in the request headers:
|
| 16 |
+
|
| 17 |
+
```
|
| 18 |
+
Authorization: Bearer <your-token>
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
**Public endpoints** (no authentication required):
|
| 22 |
+
|
| 23 |
+
- `GET /auth/health`
|
| 24 |
+
- `GET /system/health`
|
| 25 |
+
|
| 26 |
+
**Optional authentication** (enhanced data for authenticated users):
|
| 27 |
+
|
| 28 |
+
- All other `/system/*` endpoints
|
| 29 |
+
|
| 30 |
+
## Common Response Formats
|
| 31 |
+
|
| 32 |
+
### Success Response
|
| 33 |
+
|
| 34 |
+
```json
|
| 35 |
+
{
|
| 36 |
+
"success": true,
|
| 37 |
+
"data": {
|
| 38 |
+
"id": "12345",
|
| 39 |
+
"status": "completed"
|
| 40 |
+
},
|
| 41 |
+
"message": "Operation completed successfully"
|
| 42 |
+
}
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### Error Response
|
| 46 |
+
|
| 47 |
+
```json
|
| 48 |
+
{
|
| 49 |
+
"success": false,
|
| 50 |
+
"error": {
|
| 51 |
+
"code": "AUTH_INVALID",
|
| 52 |
+
"details": "Token has expired or is malformed"
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
### Pagination Format
|
| 58 |
+
|
| 59 |
+
```json
|
| 60 |
+
{
|
| 61 |
+
"success": true,
|
| 62 |
+
"data": {
|
| 63 |
+
"items": [...],
|
| 64 |
+
"pagination": {
|
| 65 |
+
"page": 1,
|
| 66 |
+
"items_per_page": 20,
|
| 67 |
+
"total_items": 150,
|
| 68 |
+
"total_pages": 8,
|
| 69 |
+
"has_next": true,
|
| 70 |
+
"has_previous": false,
|
| 71 |
+
"next_page": 2,
|
| 72 |
+
"previous_page": null
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
## Authentication Endpoints
|
| 79 |
+
|
| 80 |
+
### Health Check
|
| 81 |
+
|
| 82 |
+
- **Endpoint**: `GET /auth/health`
|
| 83 |
+
- **Description**: Check authentication service health
|
| 84 |
+
- **Authentication**: Not required
|
| 85 |
+
- **Response**: Service health status
|
| 86 |
+
- **Example Response**:
|
| 87 |
+
|
| 88 |
+
```json
|
| 89 |
+
{
|
| 90 |
+
"status": "healthy",
|
| 91 |
+
"clerk": {
|
| 92 |
+
"status": "healthy",
|
| 93 |
+
"response_time": "45ms",
|
| 94 |
+
"last_check": "2024-01-15T10:30:00Z"
|
| 95 |
+
},
|
| 96 |
+
"message": "Authentication service health check completed"
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Get Authentication Status
|
| 101 |
+
|
| 102 |
+
- **Endpoint**: `GET /auth/status`
|
| 103 |
+
- **Description**: Get current user authentication status
|
| 104 |
+
- **Headers**:
|
| 105 |
+
- `Authorization: Bearer <token>` (optional)
|
| 106 |
+
- **Response**: Authentication status and user info
|
| 107 |
+
- **Example Response (Authenticated)**:
|
| 108 |
+
|
| 109 |
+
```json
|
| 110 |
+
{
|
| 111 |
+
"authenticated": true,
|
| 112 |
+
"user_id": "user_12345",
|
| 113 |
+
"email": "[email protected]",
|
| 114 |
+
"email_verified": true,
|
| 115 |
+
"request_context": {
|
| 116 |
+
"path": "/auth/status",
|
| 117 |
+
"method": "GET",
|
| 118 |
+
"client_ip": "192.168.1.100"
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
- **Example Response (Not Authenticated)**:
|
| 124 |
+
|
| 125 |
+
```json
|
| 126 |
+
{
|
| 127 |
+
"authenticated": false,
|
| 128 |
+
"message": "No authentication provided",
|
| 129 |
+
"request_context": {
|
| 130 |
+
"path": "/auth/status",
|
| 131 |
+
"method": "GET",
|
| 132 |
+
"client_ip": "192.168.1.100"
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
### Get Current User Profile
|
| 138 |
+
|
| 139 |
+
- **Endpoint**: `GET /auth/me`
|
| 140 |
+
- **Description**: Get authenticated user's profile information
|
| 141 |
+
- **Headers**:
|
| 142 |
+
- `Authorization: Bearer <token>` (required)
|
| 143 |
+
- **Response**: User profile data
|
| 144 |
+
- **Example Response**:
|
| 145 |
+
|
| 146 |
+
```json
|
| 147 |
+
{
|
| 148 |
+
"id": "user_12345",
|
| 149 |
+
"username": "john_doe",
|
| 150 |
+
"full_name": "John Doe",
|
| 151 |
+
"email": "[email protected]",
|
| 152 |
+
"image_url": "https://example.com/avatar.jpg",
|
| 153 |
+
"email_verified": true,
|
| 154 |
+
"created_at": "2024-01-01T00:00:00Z",
|
| 155 |
+
"last_sign_in_at": "2024-01-15T10:30:00Z"
|
| 156 |
+
}
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
**Note**: `created_at` and `last_sign_in_at` fields may be `null` if not available from the authentication provider.
|
| 160 |
+
|
| 161 |
+
### Get User Permissions
|
| 162 |
+
|
| 163 |
+
- **Endpoint**: `GET /auth/permissions`
|
| 164 |
+
- **Description**: Get user's permissions and access levels
|
| 165 |
+
- **Headers**:
|
| 166 |
+
- `Authorization: Bearer <token>` (required)
|
| 167 |
+
- **Response**: User permissions and role information
|
| 168 |
+
- **Example Response**:
|
| 169 |
+
|
| 170 |
+
```json
|
| 171 |
+
{
|
| 172 |
+
"user_id": "user_12345",
|
| 173 |
+
"role": "USER",
|
| 174 |
+
"permissions": [
|
| 175 |
+
"read_files",
|
| 176 |
+
"upload_files",
|
| 177 |
+
"generate_videos"
|
| 178 |
+
],
|
| 179 |
+
"access_level": "standard"
|
| 180 |
+
}
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
### Test Protected Endpoint
|
| 184 |
+
|
| 185 |
+
- **Endpoint**: `GET /auth/test-protected`
|
| 186 |
+
- **Description**: Test endpoint for authenticated users
|
| 187 |
+
- **Headers**:
|
| 188 |
+
- `Authorization: Bearer <token>` (required)
|
| 189 |
+
- **Response**: Test response with user ID
|
| 190 |
+
- **Example Response**:
|
| 191 |
+
|
| 192 |
+
```json
|
| 193 |
+
{
|
| 194 |
+
"message": "Successfully accessed protected endpoint",
|
| 195 |
+
"user_id": "user_12345",
|
| 196 |
+
"timestamp": "2024-01-15T10:30:00Z"
|
| 197 |
+
}
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### Test Verified Endpoint
|
| 201 |
+
|
| 202 |
+
- **Endpoint**: `GET /auth/test-verified`
|
| 203 |
+
- **Description**: Test endpoint for verified users only
|
| 204 |
+
- **Headers**:
|
| 205 |
+
- `Authorization: Bearer <token>` (required)
|
| 206 |
+
- **Response**: Test response for verified users
|
| 207 |
+
- **Example Response**:
|
| 208 |
+
|
| 209 |
+
```json
|
| 210 |
+
{
|
| 211 |
+
"message": "Successfully accessed verified user endpoint",
|
| 212 |
+
"user_id": "user_12345",
|
| 213 |
+
"email": "[email protected]",
|
| 214 |
+
"email_verified": true,
|
| 215 |
+
"timestamp": "2024-01-15T10:30:00Z"
|
| 216 |
+
}
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
### Verify Token
|
| 220 |
+
|
| 221 |
+
- **Endpoint**: `POST /auth/verify`
|
| 222 |
+
- **Description**: Verify authentication token validity
|
| 223 |
+
- **Headers**:
|
| 224 |
+
- `Authorization: Bearer <token>` (required)
|
| 225 |
+
- **Response**: Token verification status
|
| 226 |
+
- **Example Response**:
|
| 227 |
+
|
| 228 |
+
```json
|
| 229 |
+
{
|
| 230 |
+
"verified": true,
|
| 231 |
+
"user_id": "user_12345",
|
| 232 |
+
"email": "[email protected]",
|
| 233 |
+
"email_verified": true,
|
| 234 |
+
"message": "Token verified successfully"
|
| 235 |
+
}
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
## File Management Endpoints
|
| 239 |
+
|
| 240 |
+
### Upload Single File
|
| 241 |
+
|
| 242 |
+
- **Endpoint**: `POST /files/upload`
|
| 243 |
+
- **Description**: Upload a single file
|
| 244 |
+
- **Headers**:
|
| 245 |
+
- `Authorization: Bearer <token>` (required)
|
| 246 |
+
- `Content-Type: multipart/form-data`
|
| 247 |
+
- **Parameters**:
|
| 248 |
+
- `file` (file, required): File to upload
|
| 249 |
+
- `file_type` (string, optional): File type category
|
| 250 |
+
- `subdirectory` (string, optional): Target subdirectory
|
| 251 |
+
- `description` (string, optional): File description
|
| 252 |
+
- **Response**: File upload confirmation with file ID
|
| 253 |
+
|
| 254 |
+
### Batch Upload Files
|
| 255 |
+
|
| 256 |
+
- **Endpoint**: `POST /files/batch-upload`
|
| 257 |
+
- **Description**: Upload multiple files at once
|
| 258 |
+
- **Headers**:
|
| 259 |
+
- `Authorization: Bearer <token>` (required)
|
| 260 |
+
- `Content-Type: multipart/form-data`
|
| 261 |
+
- **Parameters**:
|
| 262 |
+
- `files` (file[], required): Files to upload
|
| 263 |
+
- `file_type` (string, optional): File type for all files
|
| 264 |
+
- `subdirectory` (string, optional): Subdirectory for all files
|
| 265 |
+
- `description` (string, optional): Description for all files
|
| 266 |
+
- **Response**: Batch upload results
|
| 267 |
+
|
| 268 |
+
### List Files
|
| 269 |
+
|
| 270 |
+
- **Endpoint**: `GET /files`
|
| 271 |
+
- **Description**: List user's files with pagination and filtering
|
| 272 |
+
- **Headers**:
|
| 273 |
+
- `Authorization: Bearer <token>` (required)
|
| 274 |
+
- **Query Parameters**:
|
| 275 |
+
|
| 276 |
+
| Name | Type | Required | Default | Description |
|
| 277 |
+
| ---------------- | ------- | -------- | ------- | --------------------------------------------------- |
|
| 278 |
+
| `file_type` | string | no | - | Filter by file type (document, image, video, audio) |
|
| 279 |
+
| `page` | integer | no | 1 | Page number (≥1) |
|
| 280 |
+
| `items_per_page` | integer | no | 20 | Items per page (1-100) |
|
| 281 |
+
|
| 282 |
+
- **Response**: Paginated list of files
|
| 283 |
+
- **Example Response**:
|
| 284 |
+
|
| 285 |
+
```json
|
| 286 |
+
{
|
| 287 |
+
"success": true,
|
| 288 |
+
"data": {
|
| 289 |
+
"items": [
|
| 290 |
+
{
|
| 291 |
+
"id": "file_123456",
|
| 292 |
+
"filename": "document.pdf",
|
| 293 |
+
"size": 2048576,
|
| 294 |
+
"file_type": "document",
|
| 295 |
+
"created_at": "2024-01-15T10:30:00Z"
|
| 296 |
+
}
|
| 297 |
+
],
|
| 298 |
+
"pagination": {
|
| 299 |
+
"page": 1,
|
| 300 |
+
"items_per_page": 20,
|
| 301 |
+
"total_items": 150,
|
| 302 |
+
"total_pages": 8,
|
| 303 |
+
"has_next": true
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
```
|
| 308 |
+
|
| 309 |
+
### Get File Details
|
| 310 |
+
|
| 311 |
+
- **Endpoint**: `GET /files/{file_id}`
|
| 312 |
+
- **Description**: Get comprehensive file information including content details and metadata
|
| 313 |
+
- **Headers**:
|
| 314 |
+
- `Authorization: Bearer <token>` (required)
|
| 315 |
+
- **Path Parameters**:
|
| 316 |
+
- `file_id` (string, required): Unique file identifier
|
| 317 |
+
- **Response**: Complete file details
|
| 318 |
+
- **Example Response**:
|
| 319 |
+
|
| 320 |
+
```json
|
| 321 |
+
{
|
| 322 |
+
"success": true,
|
| 323 |
+
"data": {
|
| 324 |
+
"id": "file_123456",
|
| 325 |
+
"filename": "document.pdf",
|
| 326 |
+
"size": 2048576,
|
| 327 |
+
"content_type": "application/pdf",
|
| 328 |
+
"file_type": "document",
|
| 329 |
+
"subdirectory": "uploads/2024",
|
| 330 |
+
"description": "Important document",
|
| 331 |
+
"created_at": "2024-01-15T10:30:00Z",
|
| 332 |
+
"updated_at": "2024-01-15T10:30:00Z",
|
| 333 |
+
"download_count": 5,
|
| 334 |
+
"metadata": {
|
| 335 |
+
"pages": 10,
|
| 336 |
+
"author": "John Doe",
|
| 337 |
+
"creation_date": "2024-01-15"
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
### Get File Metadata
|
| 344 |
+
|
| 345 |
+
- **Endpoint**: `GET /files/{file_id}/metadata`
|
| 346 |
+
- **Description**: Get file metadata and technical information only
|
| 347 |
+
- **Headers**:
|
| 348 |
+
- `Authorization: Bearer <token>` (required)
|
| 349 |
+
- **Path Parameters**:
|
| 350 |
+
- `file_id` (string, required): Unique file identifier
|
| 351 |
+
- **Response**: File metadata only
|
| 352 |
+
- **Example Response**:
|
| 353 |
+
|
| 354 |
+
```json
|
| 355 |
+
{
|
| 356 |
+
"success": true,
|
| 357 |
+
"data": {
|
| 358 |
+
"content_type": "application/pdf",
|
| 359 |
+
"size": 2048576,
|
| 360 |
+
"checksum": "sha256:abc123...",
|
| 361 |
+
"metadata": {
|
| 362 |
+
"pages": 10,
|
| 363 |
+
"author": "John Doe",
|
| 364 |
+
"creation_date": "2024-01-15"
|
| 365 |
+
}
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
### Download File
|
| 371 |
+
|
| 372 |
+
- **Endpoint**: `GET /files/{file_id}/download`
|
| 373 |
+
- **Description**: Download file content
|
| 374 |
+
- **Headers**:
|
| 375 |
+
- `Authorization: Bearer <token>` (required)
|
| 376 |
+
- **Path Parameters**:
|
| 377 |
+
- `file_id` (string, required): Unique file identifier
|
| 378 |
+
- **Query Parameters**:
|
| 379 |
+
- `inline` (boolean, default: false): Serve inline instead of attachment
|
| 380 |
+
- **Response**: File content
|
| 381 |
+
|
| 382 |
+
### Stream File
|
| 383 |
+
|
| 384 |
+
- **Endpoint**: `GET /files/{file_id}/stream`
|
| 385 |
+
- **Description**: Stream file content
|
| 386 |
+
- **Headers**:
|
| 387 |
+
- `Authorization: Bearer <token>` (required)
|
| 388 |
+
- **Path Parameters**:
|
| 389 |
+
- `file_id` (string, required): Unique file identifier
|
| 390 |
+
- **Query Parameters**:
|
| 391 |
+
- `quality` (string, default: "auto"): Stream quality
|
| 392 |
+
- **Response**: Streamed file content
|
| 393 |
+
|
| 394 |
+
### Get File Thumbnail
|
| 395 |
+
|
| 396 |
+
- **Endpoint**: `GET /files/{file_id}/thumbnail`
|
| 397 |
+
- **Description**: Get file thumbnail
|
| 398 |
+
- **Headers**:
|
| 399 |
+
- `Authorization: Bearer <token>` (required)
|
| 400 |
+
- **Path Parameters**:
|
| 401 |
+
- `file_id` (string, required): Unique file identifier
|
| 402 |
+
- **Query Parameters**:
|
| 403 |
+
- `size` (string, default: "medium"): Thumbnail size (small|medium|large)
|
| 404 |
+
- **Response**: Thumbnail image
|
| 405 |
+
|
| 406 |
+
### Get File Analytics
|
| 407 |
+
|
| 408 |
+
- **Endpoint**: `GET /files/{file_id}/analytics`
|
| 409 |
+
- **Description**: Get file usage analytics
|
| 410 |
+
- **Headers**:
|
| 411 |
+
- `Authorization: Bearer <token>` (required)
|
| 412 |
+
- **Path Parameters**:
|
| 413 |
+
- `file_id` (string, required): Unique file identifier
|
| 414 |
+
- **Response**: File analytics data
|
| 415 |
+
|
| 416 |
+
### Delete File
|
| 417 |
+
|
| 418 |
+
- **Endpoint**: `DELETE /files/{file_id}`
|
| 419 |
+
- **Description**: Delete a file
|
| 420 |
+
- **Headers**:
|
| 421 |
+
- `Authorization: Bearer <token>` (required)
|
| 422 |
+
- **Path Parameters**:
|
| 423 |
+
- `file_id` (string, required): Unique file identifier
|
| 424 |
+
- **Response**: Deletion confirmation
|
| 425 |
+
|
| 426 |
+
### Get File Statistics
|
| 427 |
+
|
| 428 |
+
- **Endpoint**: `GET /files/stats`
|
| 429 |
+
- **Description**: Get user's file statistics
|
| 430 |
+
- **Headers**:
|
| 431 |
+
- `Authorization: Bearer <token>` (required)
|
| 432 |
+
- **Response**: File usage statistics
|
| 433 |
+
|
| 434 |
+
### Cleanup Files
|
| 435 |
+
|
| 436 |
+
- **Endpoint**: `POST /files/cleanup`
|
| 437 |
+
- **Description**: Cleanup files based on criteria
|
| 438 |
+
- **Headers**:
|
| 439 |
+
- `Authorization: Bearer <token>` (required)
|
| 440 |
+
- `Content-Type: application/json`
|
| 441 |
+
- **Request Body**: File cleanup criteria
|
| 442 |
+
- **Response**: Cleanup results
|
| 443 |
+
|
| 444 |
+
### Secure File Access
|
| 445 |
+
|
| 446 |
+
- **Endpoint**: `GET /files/secure/{file_id}`
|
| 447 |
+
- **Description**: Access files via signed URLs
|
| 448 |
+
- **Query Parameters**:
|
| 449 |
+
- `user_id` (string, required): User ID from signed URL
|
| 450 |
+
- `expires` (string, required): Expiration timestamp
|
| 451 |
+
- `signature` (string, required): URL signature
|
| 452 |
+
- `file_type` (string, optional): File type
|
| 453 |
+
- `inline` (string, default: "false"): Serve inline
|
| 454 |
+
- `size` (string, optional): Thumbnail size
|
| 455 |
+
- `quality` (string, optional): Stream quality
|
| 456 |
+
- **Response**: Secure file access
|
| 457 |
+
|
| 458 |
+
## Job Management Endpoints
|
| 459 |
+
|
| 460 |
+
### List Jobs
|
| 461 |
+
|
| 462 |
+
- **Endpoint**: `GET /jobs`
|
| 463 |
+
- **Description**: List user's jobs with pagination and filtering
|
| 464 |
+
- **Headers**:
|
| 465 |
+
- `Authorization: Bearer <token>` (required)
|
| 466 |
+
- **Query Parameters**: Pagination and filtering parameters
|
| 467 |
+
- **Response**: Paginated list of jobs
|
| 468 |
+
|
| 469 |
+
### Get Job Details
|
| 470 |
+
|
| 471 |
+
- **Endpoint**: `GET /jobs/{job_id}`
|
| 472 |
+
- **Description**: Get comprehensive job information including status, progress, and results
|
| 473 |
+
- **Headers**:
|
| 474 |
+
- `Authorization: Bearer <token>` (required)
|
| 475 |
+
- **Path Parameters**:
|
| 476 |
+
- `job_id` (string, required): Unique job identifier
|
| 477 |
+
- **Response**: Complete job details and status
|
| 478 |
+
- **Example Response**:
|
| 479 |
+
|
| 480 |
+
```json
|
| 481 |
+
{
|
| 482 |
+
"success": true,
|
| 483 |
+
"data": {
|
| 484 |
+
"id": "job_789012",
|
| 485 |
+
"type": "video_generation",
|
| 486 |
+
"status": "completed",
|
| 487 |
+
"progress": 100,
|
| 488 |
+
"created_at": "2024-01-15T10:30:00Z",
|
| 489 |
+
"started_at": "2024-01-15T10:30:05Z",
|
| 490 |
+
"completed_at": "2024-01-15T10:35:30Z",
|
| 491 |
+
"duration": 325,
|
| 492 |
+
"parameters": {
|
| 493 |
+
"prompt": "A beautiful sunset over mountains",
|
| 494 |
+
"duration": 10,
|
| 495 |
+
"quality": "1080p"
|
| 496 |
+
},
|
| 497 |
+
"result": {
|
| 498 |
+
"file_id": "video_456789",
|
| 499 |
+
"file_size": 15728640,
|
| 500 |
+
"thumbnail_url": "/videos/job_789012/thumbnail"
|
| 501 |
+
},
|
| 502 |
+
"error": null
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
```
|
| 506 |
+
|
| 507 |
+
### Get Job Logs
|
| 508 |
+
|
| 509 |
+
- **Endpoint**: `GET /jobs/{job_id}/logs`
|
| 510 |
+
- **Description**: Get job execution logs with filtering and pagination
|
| 511 |
+
- **Headers**:
|
| 512 |
+
- `Authorization: Bearer <token>` (required)
|
| 513 |
+
- **Path Parameters**:
|
| 514 |
+
- `job_id` (string, required): Unique job identifier
|
| 515 |
+
- **Query Parameters**:
|
| 516 |
+
|
| 517 |
+
| Name | Type | Required | Default | Description |
|
| 518 |
+
| -------- | ------- | -------- | ------- | ------------------------------------------------- |
|
| 519 |
+
| `limit` | integer | no | 100 | Maximum log entries (1-1000) |
|
| 520 |
+
| `offset` | integer | no | 0 | Log entries to skip (≥0) |
|
| 521 |
+
| `level` | string | no | - | Filter by log level (DEBUG, INFO, WARNING, ERROR) |
|
| 522 |
+
|
| 523 |
+
- **Response**: Job logs with metadata
|
| 524 |
+
- **Example Response**:
|
| 525 |
+
|
| 526 |
+
```json
|
| 527 |
+
{
|
| 528 |
+
"success": true,
|
| 529 |
+
"data": {
|
| 530 |
+
"logs": [
|
| 531 |
+
{
|
| 532 |
+
"timestamp": "2024-01-15T10:30:15Z",
|
| 533 |
+
"level": "INFO",
|
| 534 |
+
"message": "Video processing started",
|
| 535 |
+
"details": {
|
| 536 |
+
"step": "initialization",
|
| 537 |
+
"progress": 0
|
| 538 |
+
}
|
| 539 |
+
},
|
| 540 |
+
{
|
| 541 |
+
"timestamp": "2024-01-15T10:30:45Z",
|
| 542 |
+
"level": "INFO",
|
| 543 |
+
"message": "Processing frame 100/1000",
|
| 544 |
+
"details": {
|
| 545 |
+
"step": "rendering",
|
| 546 |
+
"progress": 10
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
],
|
| 550 |
+
"total_logs": 250,
|
| 551 |
+
"has_more": true
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
```
|
| 555 |
+
|
| 556 |
+
### Cancel Job
|
| 557 |
+
|
| 558 |
+
- **Endpoint**: `POST /jobs/{job_id}/cancel`
|
| 559 |
+
- **Description**: Cancel a running job
|
| 560 |
+
- **Headers**:
|
| 561 |
+
- `Authorization: Bearer <token>` (required)
|
| 562 |
+
- **Path Parameters**:
|
| 563 |
+
- `job_id` (string, required): Unique job identifier
|
| 564 |
+
- **Response**: Cancellation confirmation
|
| 565 |
+
|
| 566 |
+
### Delete Job
|
| 567 |
+
|
| 568 |
+
- **Endpoint**: `DELETE /jobs/{job_id}`
|
| 569 |
+
- **Description**: Delete a job and its data
|
| 570 |
+
- **Headers**:
|
| 571 |
+
- `Authorization: Bearer <token>` (required)
|
| 572 |
+
- **Path Parameters**:
|
| 573 |
+
- `job_id` (string, required): Unique job identifier
|
| 574 |
+
- **Response**: Deletion confirmation
|
| 575 |
+
|
| 576 |
+
## System Monitoring Endpoints
|
| 577 |
+
|
| 578 |
+
### System Health Check
|
| 579 |
+
|
| 580 |
+
- **Endpoint**: `GET /system/health`
|
| 581 |
+
- **Description**: Get overall system health status
|
| 582 |
+
- **Headers**:
|
| 583 |
+
- `Authorization: Bearer <token>` (optional)
|
| 584 |
+
- **Response**: System health metrics
|
| 585 |
+
|
| 586 |
+
### System Metrics
|
| 587 |
+
|
| 588 |
+
- **Endpoint**: `GET /system/metrics`
|
| 589 |
+
- **Description**: Get detailed system metrics
|
| 590 |
+
- **Headers**:
|
| 591 |
+
- `Authorization: Bearer <token>` (optional)
|
| 592 |
+
- **Response**: System performance metrics
|
| 593 |
+
|
| 594 |
+
### Queue Status
|
| 595 |
+
|
| 596 |
+
- **Endpoint**: `GET /system/queue`
|
| 597 |
+
- **Description**: Get job queue status and statistics
|
| 598 |
+
- **Headers**:
|
| 599 |
+
- `Authorization: Bearer <token>` (optional)
|
| 600 |
+
- **Response**: Queue status and metrics
|
| 601 |
+
|
| 602 |
+
### Cache Information
|
| 603 |
+
|
| 604 |
+
- **Endpoint**: `GET /system/cache`
|
| 605 |
+
- **Description**: Get cache status and information
|
| 606 |
+
- **Headers**:
|
| 607 |
+
- `Authorization: Bearer <token>` (optional)
|
| 608 |
+
- **Response**: Cache metrics and status
|
| 609 |
+
|
| 610 |
+
### Cache Metrics
|
| 611 |
+
|
| 612 |
+
- **Endpoint**: `GET /system/cache/metrics`
|
| 613 |
+
- **Description**: Get detailed cache metrics
|
| 614 |
+
- **Headers**:
|
| 615 |
+
- `Authorization: Bearer <token>` (optional)
|
| 616 |
+
- **Response**: Cache performance metrics
|
| 617 |
+
|
| 618 |
+
### Cache Report
|
| 619 |
+
|
| 620 |
+
- **Endpoint**: `GET /system/cache/report`
|
| 621 |
+
- **Description**: Get comprehensive cache report
|
| 622 |
+
- **Headers**:
|
| 623 |
+
- `Authorization: Bearer <token>` (optional)
|
| 624 |
+
- **Response**: Detailed cache report
|
| 625 |
+
|
| 626 |
+
### Performance Summary
|
| 627 |
+
|
| 628 |
+
- **Endpoint**: `GET /system/performance`
|
| 629 |
+
- **Description**: Get system performance summary
|
| 630 |
+
- **Headers**:
|
| 631 |
+
- `Authorization: Bearer <token>` (optional)
|
| 632 |
+
- **Query Parameters**:
|
| 633 |
+
- `hours` (integer, default: 1): Time range in hours
|
| 634 |
+
- **Response**: Performance summary
|
| 635 |
+
|
| 636 |
+
### Connection Statistics
|
| 637 |
+
|
| 638 |
+
- **Endpoint**: `GET /system/connections`
|
| 639 |
+
- **Description**: Get connection statistics
|
| 640 |
+
- **Headers**:
|
| 641 |
+
- `Authorization: Bearer <token>` (optional)
|
| 642 |
+
- **Response**: Connection metrics
|
| 643 |
+
|
| 644 |
+
### Async Statistics
|
| 645 |
+
|
| 646 |
+
- **Endpoint**: `GET /system/async`
|
| 647 |
+
- **Description**: Get asynchronous processing statistics
|
| 648 |
+
- **Headers**:
|
| 649 |
+
- `Authorization: Bearer <token>` (optional)
|
| 650 |
+
- **Response**: Async processing metrics
|
| 651 |
+
|
| 652 |
+
### Deduplication Statistics
|
| 653 |
+
|
| 654 |
+
- **Endpoint**: `GET /system/deduplication`
|
| 655 |
+
- **Description**: Get deduplication statistics
|
| 656 |
+
- **Headers**:
|
| 657 |
+
- `Authorization: Bearer <token>` (optional)
|
| 658 |
+
- **Response**: Deduplication metrics
|
| 659 |
+
|
| 660 |
+
### Invalidate Cache
|
| 661 |
+
|
| 662 |
+
- **Endpoint**: `POST /system/cache/invalidate`
|
| 663 |
+
- **Description**: Invalidate cache entries
|
| 664 |
+
- **Headers**:
|
| 665 |
+
- `Authorization: Bearer <token>` (optional)
|
| 666 |
+
- **Query Parameters**:
|
| 667 |
+
- `pattern` (string, optional): Cache key pattern
|
| 668 |
+
- `user_id` (string, optional): User-specific cache
|
| 669 |
+
- **Response**: Cache invalidation results
|
| 670 |
+
|
| 671 |
+
### Warm Cache
|
| 672 |
+
|
| 673 |
+
- **Endpoint**: `POST /system/cache/warm`
|
| 674 |
+
- **Description**: Pre-warm cache with frequently accessed data
|
| 675 |
+
- **Headers**:
|
| 676 |
+
- `Authorization: Bearer <token>` (optional)
|
| 677 |
+
- **Response**: Cache warming results
|
| 678 |
+
|
| 679 |
+
### Optimize Performance
|
| 680 |
+
|
| 681 |
+
- **Endpoint**: `POST /system/optimize`
|
| 682 |
+
- **Description**: Trigger system performance optimization
|
| 683 |
+
- **Headers**:
|
| 684 |
+
- `Authorization: Bearer <token>` (optional)
|
| 685 |
+
- **Response**: Optimization results
|
| 686 |
+
|
| 687 |
+
## Video Processing Endpoints
|
| 688 |
+
|
| 689 |
+
### Generate Video
|
| 690 |
+
|
| 691 |
+
- **Endpoint**: `POST /videos/generate`
|
| 692 |
+
- **Description**: Create a new video generation job
|
| 693 |
+
- **Headers**:
|
| 694 |
+
- `Authorization: Bearer <token>` (required)
|
| 695 |
+
- `Content-Type: application/json`
|
| 696 |
+
- **Request Body**: Job creation parameters
|
| 697 |
+
- **Response**: Job creation confirmation with job ID
|
| 698 |
+
|
| 699 |
+
### Get Job Status
|
| 700 |
+
|
| 701 |
+
- **Endpoint**: `GET /videos/{job_id}/status`
|
| 702 |
+
- **Description**: Get video generation job status
|
| 703 |
+
- **Headers**:
|
| 704 |
+
- `Authorization: Bearer <token>` (required)
|
| 705 |
+
- **Path Parameters**:
|
| 706 |
+
- `job_id` (string, required): Unique job identifier
|
| 707 |
+
- **Response**: Job status and progress
|
| 708 |
+
|
| 709 |
+
### Download Video
|
| 710 |
+
|
| 711 |
+
- **Endpoint**: `GET /videos/{job_id}/download`
|
| 712 |
+
- **Description**: Download generated video
|
| 713 |
+
- **Headers**:
|
| 714 |
+
- `Authorization: Bearer <token>` (required)
|
| 715 |
+
- **Path Parameters**:
|
| 716 |
+
- `job_id` (string, required): Unique job identifier
|
| 717 |
+
- **Query Parameters**:
|
| 718 |
+
- `inline` (boolean, default: false): Serve inline instead of attachment
|
| 719 |
+
- **Response**: Video file content
|
| 720 |
+
|
| 721 |
+
### Stream Video
|
| 722 |
+
|
| 723 |
+
- **Endpoint**: `GET /videos/{job_id}/stream`
|
| 724 |
+
- **Description**: Stream generated video
|
| 725 |
+
- **Headers**:
|
| 726 |
+
- `Authorization: Bearer <token>` (required)
|
| 727 |
+
- **Path Parameters**:
|
| 728 |
+
- `job_id` (string, required): Unique job identifier
|
| 729 |
+
- **Query Parameters**:
|
| 730 |
+
- `quality` (string, default: "auto"): Stream quality (auto|720p|1080p)
|
| 731 |
+
- **Response**: Streamed video content
|
| 732 |
+
|
| 733 |
+
### Get Video Metadata
|
| 734 |
+
|
| 735 |
+
- **Endpoint**: `GET /videos/{job_id}/metadata`
|
| 736 |
+
- **Description**: Get comprehensive video metadata and technical information
|
| 737 |
+
- **Headers**:
|
| 738 |
+
- `Authorization: Bearer <token>` (required)
|
| 739 |
+
- **Path Parameters**:
|
| 740 |
+
- `job_id` (string, required): Unique job identifier
|
| 741 |
+
- **Response**: Detailed video metadata
|
| 742 |
+
- **Example Response**:
|
| 743 |
+
|
| 744 |
+
```json
|
| 745 |
+
{
|
| 746 |
+
"success": true,
|
| 747 |
+
"data": {
|
| 748 |
+
"job_id": "job_789012",
|
| 749 |
+
"video": {
|
| 750 |
+
"duration": 10.5,
|
| 751 |
+
"width": 1920,
|
| 752 |
+
"height": 1080,
|
| 753 |
+
"fps": 30,
|
| 754 |
+
"bitrate": 5000000,
|
| 755 |
+
"codec": "h264",
|
| 756 |
+
"format": "mp4",
|
| 757 |
+
"file_size": 15728640
|
| 758 |
+
},
|
| 759 |
+
"audio": {
|
| 760 |
+
"codec": "aac",
|
| 761 |
+
"bitrate": 128000,
|
| 762 |
+
"sample_rate": 44100,
|
| 763 |
+
"channels": 2
|
| 764 |
+
},
|
| 765 |
+
"generation": {
|
| 766 |
+
"prompt": "A beautiful sunset over mountains",
|
| 767 |
+
"model": "t2v-v2.1",
|
| 768 |
+
"seed": 12345,
|
| 769 |
+
"created_at": "2024-01-15T10:30:00Z"
|
| 770 |
+
}
|
| 771 |
+
}
|
| 772 |
+
}
|
| 773 |
+
```
|
| 774 |
+
|
| 775 |
+
### Get Video Thumbnail
|
| 776 |
+
|
| 777 |
+
- **Endpoint**: `GET /videos/{job_id}/thumbnail`
|
| 778 |
+
- **Description**: Get video thumbnail
|
| 779 |
+
- **Headers**:
|
| 780 |
+
- `Authorization: Bearer <token>` (required)
|
| 781 |
+
- **Path Parameters**:
|
| 782 |
+
- `job_id` (string, required): Unique job identifier
|
| 783 |
+
- **Query Parameters**:
|
| 784 |
+
- `size` (string, default: "medium"): Thumbnail size (small|medium|large)
|
| 785 |
+
- **Response**: Video thumbnail image
|
| 786 |
+
|
| 787 |
+
## Error Handling
|
| 788 |
+
|
| 789 |
+
### Error Codes
|
| 790 |
+
|
| 791 |
+
| Code | HTTP Status | Description |
|
| 792 |
+
| --------------------- | ----------- | ------------------------------- |
|
| 793 |
+
| `AUTH_REQUIRED` | 401 | Authentication required |
|
| 794 |
+
| `AUTH_INVALID` | 401 | Invalid authentication token |
|
| 795 |
+
| `AUTH_EXPIRED` | 401 | Authentication token expired |
|
| 796 |
+
| `PERMISSION_DENIED` | 403 | Insufficient permissions |
|
| 797 |
+
| `RESOURCE_NOT_FOUND` | 404 | Requested resource not found |
|
| 798 |
+
| `VALIDATION_ERROR` | 400 | Request validation failed |
|
| 799 |
+
| `RATE_LIMIT_EXCEEDED` | 429 | Rate limit exceeded |
|
| 800 |
+
| `SERVER_ERROR` | 500 | Internal server error |
|
| 801 |
+
| `SERVICE_UNAVAILABLE` | 503 | Service temporarily unavailable |
|
| 802 |
+
|
| 803 |
+
### Error Response Examples
|
| 804 |
+
|
| 805 |
+
**Invalid Authentication (401)**
|
| 806 |
+
|
| 807 |
+
```json
|
| 808 |
+
{
|
| 809 |
+
"success": false,
|
| 810 |
+
"error": {
|
| 811 |
+
"code": "AUTH_INVALID",
|
| 812 |
+
"details": "Token has expired or is malformed"
|
| 813 |
+
}
|
| 814 |
+
}
|
| 815 |
+
```
|
| 816 |
+
|
| 817 |
+
**Resource Not Found (404)**
|
| 818 |
+
|
| 819 |
+
```json
|
| 820 |
+
{
|
| 821 |
+
"success": false,
|
| 822 |
+
"error": {
|
| 823 |
+
"code": "RESOURCE_NOT_FOUND",
|
| 824 |
+
"message": "File not found",
|
| 825 |
+
"details": "File with ID 'file_123456' does not exist or you don't have access"
|
| 826 |
+
}
|
| 827 |
+
}
|
| 828 |
+
```
|
| 829 |
+
|
| 830 |
+
**Validation Error (400)**
|
| 831 |
+
|
| 832 |
+
```json
|
| 833 |
+
{
|
| 834 |
+
"success": false,
|
| 835 |
+
"error": {
|
| 836 |
+
"code": "VALIDATION_ERROR",
|
| 837 |
+
"message": "Request validation failed",
|
| 838 |
+
"details": {
|
| 839 |
+
"file_type": [
|
| 840 |
+
"Invalid file type. Must be one of: document, image, video, audio"
|
| 841 |
+
],
|
| 842 |
+
"page": ["Page must be greater than 0"]
|
| 843 |
+
}
|
| 844 |
+
}
|
| 845 |
+
}
|
| 846 |
+
```
|
| 847 |
+
|
| 848 |
+
**Rate Limit Exceeded (429)**
|
| 849 |
+
|
| 850 |
+
```json
|
| 851 |
+
{
|
| 852 |
+
"success": false,
|
| 853 |
+
"error": {
|
| 854 |
+
"code": "RATE_LIMIT_EXCEEDED",
|
| 855 |
+
"message": "Rate limit exceeded",
|
| 856 |
+
"details": "You have exceeded the limit of 100 uploads per hour. Try again in 45 minutes."
|
| 857 |
+
}
|
| 858 |
+
}
|
| 859 |
+
```
|
| 860 |
+
|
| 861 |
+
## Rate Limits
|
| 862 |
+
|
| 863 |
+
- **General API**: 1000 requests per hour per user
|
| 864 |
+
- **File Upload**: 100 uploads per hour per user
|
| 865 |
+
- **Video Generation**: 10 jobs per hour per user
|
| 866 |
+
- **System Endpoints**: 500 requests per hour per user
|
| 867 |
+
|
| 868 |
+
## Code Examples
|
| 869 |
+
|
| 870 |
+
### cURL Examples
|
| 871 |
+
|
| 872 |
+
**Upload a file**
|
| 873 |
+
|
| 874 |
+
```bash
|
| 875 |
+
curl -X POST "https://api.example.com/api/v1/files/upload" \
|
| 876 |
+
-H "Authorization: Bearer your-token-here" \
|
| 877 |
+
-F "[email protected]" \
|
| 878 |
+
-F "file_type=document" \
|
| 879 |
+
-F "description=Sample document"
|
| 880 |
+
```
|
| 881 |
+
|
| 882 |
+
**Generate video**
|
| 883 |
+
|
| 884 |
+
```bash
|
| 885 |
+
curl -X POST "https://api.example.com/api/v1/videos/generate" \
|
| 886 |
+
-H "Authorization: Bearer your-token-here" \
|
| 887 |
+
-H "Content-Type: application/json" \
|
| 888 |
+
-d '{
|
| 889 |
+
"prompt": "A beautiful sunset over mountains",
|
| 890 |
+
"duration": 10,
|
| 891 |
+
"quality": "1080p"
|
| 892 |
+
}'
|
| 893 |
+
```
|
| 894 |
+
|
| 895 |
+
**Get job status**
|
| 896 |
+
|
| 897 |
+
```bash
|
| 898 |
+
curl -X GET "https://api.example.com/api/v1/jobs/job_789012" \
|
| 899 |
+
-H "Authorization: Bearer your-token-here"
|
| 900 |
+
```
|
| 901 |
+
|
| 902 |
+
**Get system metrics**
|
| 903 |
+
|
| 904 |
+
```bash
|
| 905 |
+
curl -X GET "https://api.example.com/api/v1/system/metrics" \
|
| 906 |
+
-H "Authorization: Bearer your-token-here"
|
| 907 |
+
```
|
| 908 |
+
|
| 909 |
+
### Python Examples
|
| 910 |
+
|
| 911 |
+
**File Management**
|
| 912 |
+
|
| 913 |
+
```python
|
| 914 |
+
import requests
|
| 915 |
+
|
| 916 |
+
headers = {"Authorization": "Bearer your-token-here"}
|
| 917 |
+
base_url = "https://api.example.com/api/v1"
|
| 918 |
+
|
| 919 |
+
# Upload file
|
| 920 |
+
with open("example.txt", "rb") as f:
|
| 921 |
+
files = {"file": f}
|
| 922 |
+
data = {"file_type": "document", "description": "Sample document"}
|
| 923 |
+
response = requests.post(f"{base_url}/files/upload", headers=headers, files=files, data=data)
|
| 924 |
+
file_data = response.json()
|
| 925 |
+
print(f"File uploaded: {file_data['data']['id']}")
|
| 926 |
+
|
| 927 |
+
# List files
|
| 928 |
+
response = requests.get(f"{base_url}/files", headers=headers, params={"page": 1, "items_per_page": 10})
|
| 929 |
+
files = response.json()
|
| 930 |
+
print(f"Found {files['data']['pagination']['total_items']} files")
|
| 931 |
+
```
|
| 932 |
+
|
| 933 |
+
**Job Management**
|
| 934 |
+
|
| 935 |
+
```python
|
| 936 |
+
# Get job logs
|
| 937 |
+
job_id = "job_789012"
|
| 938 |
+
response = requests.get(f"{base_url}/jobs/{job_id}/logs", headers=headers, params={"limit": 50, "level": "INFO"})
|
| 939 |
+
logs = response.json()
|
| 940 |
+
print(f"Retrieved {len(logs['data']['logs'])} log entries")
|
| 941 |
+
|
| 942 |
+
# Cancel job
|
| 943 |
+
response = requests.post(f"{base_url}/jobs/{job_id}/cancel", headers=headers)
|
| 944 |
+
if response.json()['success']:
|
| 945 |
+
print("Job cancelled successfully")
|
| 946 |
+
```
|
| 947 |
+
|
| 948 |
+
**Video Processing**
|
| 949 |
+
|
| 950 |
+
```python
|
| 951 |
+
# Generate video
|
| 952 |
+
job_data = {
|
| 953 |
+
"prompt": "A beautiful sunset over mountains",
|
| 954 |
+
"duration": 10,
|
| 955 |
+
"quality": "1080p"
|
| 956 |
+
}
|
| 957 |
+
response = requests.post(f"{base_url}/videos/generate", headers=headers, json=job_data)
|
| 958 |
+
job = response.json()
|
| 959 |
+
job_id = job['data']['job_id']
|
| 960 |
+
print(f"Video generation started: {job_id}")
|
| 961 |
+
|
| 962 |
+
# Check status
|
| 963 |
+
response = requests.get(f"{base_url}/videos/{job_id}/status", headers=headers)
|
| 964 |
+
status = response.json()
|
| 965 |
+
print(f"Job status: {status['data']['status']} ({status['data']['progress']}%)")
|
| 966 |
+
```
|
| 967 |
+
|
| 968 |
+
**System Monitoring**
|
| 969 |
+
|
| 970 |
+
```python
|
| 971 |
+
# Get system metrics
|
| 972 |
+
response = requests.get(f"{base_url}/system/metrics", headers=headers)
|
| 973 |
+
metrics = response.json()
|
| 974 |
+
print(f"CPU usage: {metrics['data']['cpu_usage']}%")
|
| 975 |
+
print(f"Memory usage: {metrics['data']['memory_usage']}%")
|
| 976 |
+
|
| 977 |
+
# Get queue status
|
| 978 |
+
response = requests.get(f"{base_url}/system/queue", headers=headers)
|
| 979 |
+
queue = response.json()
|
| 980 |
+
print(f"Jobs in queue: {queue['data']['pending_jobs']}")
|
| 981 |
+
```
|
| 982 |
+
|
| 983 |
+
### JavaScript Examples
|
| 984 |
+
|
| 985 |
+
**File Management**
|
| 986 |
+
|
| 987 |
+
```javascript
|
| 988 |
+
const headers = {
|
| 989 |
+
Authorization: "Bearer your-token-here",
|
| 990 |
+
};
|
| 991 |
+
const baseUrl = "https://api.example.com/api/v1";
|
| 992 |
+
|
| 993 |
+
// Upload file
|
| 994 |
+
const formData = new FormData();
|
| 995 |
+
formData.append("file", fileInput.files[0]);
|
| 996 |
+
formData.append("file_type", "document");
|
| 997 |
+
formData.append("description", "Sample document");
|
| 998 |
+
|
| 999 |
+
fetch(`${baseUrl}/files/upload`, {
|
| 1000 |
+
method: "POST",
|
| 1001 |
+
headers: headers,
|
| 1002 |
+
body: formData,
|
| 1003 |
+
})
|
| 1004 |
+
.then((response) => response.json())
|
| 1005 |
+
.then((data) => console.log("File uploaded:", data.data.id));
|
| 1006 |
+
|
| 1007 |
+
// List files with pagination
|
| 1008 |
+
fetch(`${baseUrl}/files?page=1&items_per_page=10`, {
|
| 1009 |
+
headers: headers,
|
| 1010 |
+
})
|
| 1011 |
+
.then((response) => response.json())
|
| 1012 |
+
.then((data) =>
|
| 1013 |
+
console.log(`Found ${data.data.pagination.total_items} files`)
|
| 1014 |
+
);
|
| 1015 |
+
```
|
| 1016 |
+
|
| 1017 |
+
**Job Management**
|
| 1018 |
+
|
| 1019 |
+
```javascript
|
| 1020 |
+
// Get job logs
|
| 1021 |
+
const jobId = "job_789012";
|
| 1022 |
+
fetch(`${baseUrl}/jobs/${jobId}/logs?limit=50&level=INFO`, {
|
| 1023 |
+
headers: headers,
|
| 1024 |
+
})
|
| 1025 |
+
.then((response) => response.json())
|
| 1026 |
+
.then((data) =>
|
| 1027 |
+
console.log(`Retrieved ${data.data.logs.length} log entries`)
|
| 1028 |
+
);
|
| 1029 |
+
|
| 1030 |
+
// Cancel job
|
| 1031 |
+
fetch(`${baseUrl}/jobs/${jobId}/cancel`, {
|
| 1032 |
+
method: "POST",
|
| 1033 |
+
headers: headers,
|
| 1034 |
+
})
|
| 1035 |
+
.then((response) => response.json())
|
| 1036 |
+
.then((data) => {
|
| 1037 |
+
if (data.success) console.log("Job cancelled successfully");
|
| 1038 |
+
});
|
| 1039 |
+
```
|
| 1040 |
+
|
| 1041 |
+
**Video Processing**
|
| 1042 |
+
|
| 1043 |
+
```javascript
|
| 1044 |
+
// Generate video
|
| 1045 |
+
const jobData = {
|
| 1046 |
+
prompt: "A beautiful sunset over mountains",
|
| 1047 |
+
duration: 10,
|
| 1048 |
+
quality: "1080p",
|
| 1049 |
+
};
|
| 1050 |
+
|
| 1051 |
+
fetch(`${baseUrl}/videos/generate`, {
|
| 1052 |
+
method: "POST",
|
| 1053 |
+
headers: { ...headers, "Content-Type": "application/json" },
|
| 1054 |
+
body: JSON.stringify(jobData),
|
| 1055 |
+
})
|
| 1056 |
+
.then((response) => response.json())
|
| 1057 |
+
.then((data) => {
|
| 1058 |
+
const jobId = data.data.job_id;
|
| 1059 |
+
console.log("Video generation started:", jobId);
|
| 1060 |
+
|
| 1061 |
+
// Poll for status
|
| 1062 |
+
const checkStatus = () => {
|
| 1063 |
+
fetch(`${baseUrl}/videos/${jobId}/status`, { headers })
|
| 1064 |
+
.then((response) => response.json())
|
| 1065 |
+
.then((status) => {
|
| 1066 |
+
console.log(
|
| 1067 |
+
`Status: ${status.data.status} (${status.data.progress}%)`
|
| 1068 |
+
);
|
| 1069 |
+
if (status.data.status === "processing") {
|
| 1070 |
+
setTimeout(checkStatus, 5000); // Check again in 5 seconds
|
| 1071 |
+
}
|
| 1072 |
+
});
|
| 1073 |
+
};
|
| 1074 |
+
checkStatus();
|
| 1075 |
+
});
|
| 1076 |
+
```
|
| 1077 |
+
|
| 1078 |
+
**System Monitoring**
|
| 1079 |
+
|
| 1080 |
+
```javascript
|
| 1081 |
+
// Get system metrics
|
| 1082 |
+
fetch(`${baseUrl}/system/metrics`, { headers })
|
| 1083 |
+
.then((response) => response.json())
|
| 1084 |
+
.then((data) => {
|
| 1085 |
+
console.log(`CPU usage: ${data.data.cpu_usage}%`);
|
| 1086 |
+
console.log(`Memory usage: ${data.data.memory_usage}%`);
|
| 1087 |
+
});
|
| 1088 |
+
|
| 1089 |
+
// Get queue status
|
| 1090 |
+
fetch(`${baseUrl}/system/queue`, { headers })
|
| 1091 |
+
.then((response) => response.json())
|
| 1092 |
+
.then((data) => console.log(`Jobs in queue: ${data.data.pending_jobs}`));
|
| 1093 |
+
```
|
| 1094 |
+
|
| 1095 |
+
## Support
|
| 1096 |
+
|
| 1097 |
+
For API support and questions, please contact:
|
| 1098 |
+
|
| 1099 |
+
- Email: [email protected]
|
| 1100 |
+
- Documentation: https://docs.example.com
|
| 1101 |
+
- Status Page: https://status.example.com
|
| 1102 |
+
|
| 1103 |
+
## Webhooks
|
| 1104 |
+
|
| 1105 |
+
The T2M API supports webhook notifications for long-running operations like video generation.
|
| 1106 |
+
|
| 1107 |
+
### Webhook Configuration
|
| 1108 |
+
|
| 1109 |
+
Configure webhook URLs in your account settings or via the API:
|
| 1110 |
+
|
| 1111 |
+
```bash
|
| 1112 |
+
curl -X POST "https://api.example.com/api/v1/webhooks" \
|
| 1113 |
+
-H "Authorization: Bearer your-token-here" \
|
| 1114 |
+
-H "Content-Type: application/json" \
|
| 1115 |
+
-d '{
|
| 1116 |
+
"url": "https://your-app.com/webhooks/t2m",
|
| 1117 |
+
"events": ["job.completed", "job.failed"],
|
| 1118 |
+
"secret": "your-webhook-secret"
|
| 1119 |
+
}'
|
| 1120 |
+
```
|
| 1121 |
+
|
| 1122 |
+
### Webhook Events
|
| 1123 |
+
|
| 1124 |
+
| Event | Description |
|
| 1125 |
+
| --------------- | ------------------------------- |
|
| 1126 |
+
| `job.started` | Job processing has begun |
|
| 1127 |
+
| `job.progress` | Job progress update (every 10%) |
|
| 1128 |
+
| `job.completed` | Job completed successfully |
|
| 1129 |
+
| `job.failed` | Job failed with error |
|
| 1130 |
+
| `job.cancelled` | Job was cancelled |
|
| 1131 |
+
|
| 1132 |
+
### Webhook Payload Example
|
| 1133 |
+
|
| 1134 |
+
**Job Completed**
|
| 1135 |
+
|
| 1136 |
+
```json
|
| 1137 |
+
{
|
| 1138 |
+
"event": "job.completed",
|
| 1139 |
+
"timestamp": "2024-01-15T10:35:30Z",
|
| 1140 |
+
"data": {
|
| 1141 |
+
"job_id": "job_789012",
|
| 1142 |
+
"type": "video_generation",
|
| 1143 |
+
"status": "completed",
|
| 1144 |
+
"result": {
|
| 1145 |
+
"file_id": "video_456789",
|
| 1146 |
+
"download_url": "https://api.example.com/api/v1/videos/job_789012/download",
|
| 1147 |
+
"thumbnail_url": "https://api.example.com/api/v1/videos/job_789012/thumbnail"
|
| 1148 |
+
}
|
| 1149 |
+
}
|
| 1150 |
+
}
|
| 1151 |
+
```
|
| 1152 |
+
|
| 1153 |
+
### Webhook Security
|
| 1154 |
+
|
| 1155 |
+
Verify webhook authenticity using the signature header:
|
| 1156 |
+
|
| 1157 |
+
```python
|
| 1158 |
+
import hmac
|
| 1159 |
+
import hashlib
|
| 1160 |
+
|
| 1161 |
+
def verify_webhook(payload, signature, secret):
|
| 1162 |
+
expected = hmac.new(
|
| 1163 |
+
secret.encode('utf-8'),
|
| 1164 |
+
payload.encode('utf-8'),
|
| 1165 |
+
hashlib.sha256
|
| 1166 |
+
).hexdigest()
|
| 1167 |
+
return hmac.compare_digest(f"sha256={expected}", signature)
|
| 1168 |
+
|
| 1169 |
+
# In your webhook handler
|
| 1170 |
+
signature = request.headers.get('X-T2M-Signature')
|
| 1171 |
+
if verify_webhook(request.body, signature, webhook_secret):
|
| 1172 |
+
# Process webhook
|
| 1173 |
+
pass
|
| 1174 |
+
```
|
| 1175 |
+
|
| 1176 |
+
## Advanced Features
|
| 1177 |
+
|
| 1178 |
+
### Batch Operations
|
| 1179 |
+
|
| 1180 |
+
**Batch File Upload**
|
| 1181 |
+
|
| 1182 |
+
```bash
|
| 1183 |
+
curl -X POST "https://api.example.com/api/v1/files/batch-upload" \
|
| 1184 |
+
-H "Authorization: Bearer your-token-here" \
|
| 1185 |
+
-F "[email protected]" \
|
| 1186 |
+
-F "[email protected]" \
|
| 1187 |
+
-F "[email protected]" \
|
| 1188 |
+
-F "file_type=document"
|
| 1189 |
+
```
|
| 1190 |
+
|
| 1191 |
+
### Signed URLs for Secure Access
|
| 1192 |
+
|
| 1193 |
+
Generate temporary signed URLs for file access without authentication:
|
| 1194 |
+
|
| 1195 |
+
```python
|
| 1196 |
+
# Request signed URL
|
| 1197 |
+
response = requests.post(f"{base_url}/files/{file_id}/signed-url",
|
| 1198 |
+
headers=headers,
|
| 1199 |
+
json={"expires_in": 3600}) # 1 hour
|
| 1200 |
+
signed_url = response.json()['data']['url']
|
| 1201 |
+
|
| 1202 |
+
# Use signed URL (no auth required)
|
| 1203 |
+
file_response = requests.get(signed_url)
|
| 1204 |
+
```
|
| 1205 |
+
|
| 1206 |
+
### Streaming and Quality Options
|
| 1207 |
+
|
| 1208 |
+
**Video Streaming with Quality Selection**
|
| 1209 |
+
|
| 1210 |
+
```bash
|
| 1211 |
+
# Stream in different qualities
|
| 1212 |
+
curl "https://api.example.com/api/v1/videos/job_789012/stream?quality=720p" \
|
| 1213 |
+
-H "Authorization: Bearer your-token-here"
|
| 1214 |
+
```
|
| 1215 |
+
|
| 1216 |
+
**Thumbnail Sizes**
|
| 1217 |
+
|
| 1218 |
+
```bash
|
| 1219 |
+
# Get different thumbnail sizes
|
| 1220 |
+
curl "https://api.example.com/api/v1/videos/job_789012/thumbnail?size=large" \
|
| 1221 |
+
-H "Authorization: Bearer your-token-here"
|
| 1222 |
+
```
|
| 1223 |
+
|
| 1224 |
+
## Performance Optimization
|
| 1225 |
+
|
| 1226 |
+
### Caching
|
| 1227 |
+
|
| 1228 |
+
The API implements intelligent caching. Use these endpoints to manage cache:
|
| 1229 |
+
|
| 1230 |
+
```bash
|
| 1231 |
+
# Warm cache for better performance
|
| 1232 |
+
curl -X POST "https://api.example.com/api/v1/system/cache/warm" \
|
| 1233 |
+
-H "Authorization: Bearer your-token-here"
|
| 1234 |
+
|
| 1235 |
+
# Invalidate specific cache patterns
|
| 1236 |
+
curl -X POST "https://api.example.com/api/v1/system/cache/invalidate" \
|
| 1237 |
+
-H "Authorization: Bearer your-token-here" \
|
| 1238 |
+
-d "pattern=user:123:*"
|
| 1239 |
+
```
|
| 1240 |
+
|
| 1241 |
+
### Request Optimization
|
| 1242 |
+
|
| 1243 |
+
- Use pagination for large datasets
|
| 1244 |
+
- Implement client-side caching for frequently accessed data
|
| 1245 |
+
- Use appropriate quality settings for streaming
|
| 1246 |
+
- Batch operations when possible
|
| 1247 |
+
|
| 1248 |
+
## Monitoring and Analytics
|
| 1249 |
+
|
| 1250 |
+
### System Health Monitoring
|
| 1251 |
+
|
| 1252 |
+
```bash
|
| 1253 |
+
# Check overall system health
|
| 1254 |
+
curl "https://api.example.com/api/v1/system/health"
|
| 1255 |
+
|
| 1256 |
+
# Get detailed performance metrics
|
| 1257 |
+
curl "https://api.example.com/api/v1/system/performance?hours=24" \
|
| 1258 |
+
-H "Authorization: Bearer your-token-here"
|
| 1259 |
+
```
|
| 1260 |
+
|
| 1261 |
+
### File Analytics
|
| 1262 |
+
|
| 1263 |
+
Track file usage and performance:
|
| 1264 |
+
|
| 1265 |
+
```bash
|
| 1266 |
+
curl "https://api.example.com/api/v1/files/file_123456/analytics" \
|
| 1267 |
+
-H "Authorization: Bearer your-token-here"
|
| 1268 |
+
```
|
| 1269 |
+
|
| 1270 |
+
## Migration Guide
|
| 1271 |
+
|
| 1272 |
+
### From v1.0 to v1.1
|
| 1273 |
+
|
| 1274 |
+
**Breaking Changes:**
|
| 1275 |
+
|
| 1276 |
+
- `GET /files/{id}/info` is now `GET /files/{id}` (consolidated endpoints)
|
| 1277 |
+
- Error response format now includes `details` field
|
| 1278 |
+
- Pagination format standardized across all endpoints
|
| 1279 |
+
|
| 1280 |
+
**New Features:**
|
| 1281 |
+
|
| 1282 |
+
- Webhook support for job notifications
|
| 1283 |
+
- Batch file operations
|
| 1284 |
+
- Enhanced error details
|
| 1285 |
+
- Signed URL support
|
| 1286 |
+
|
| 1287 |
+
**Migration Steps:**
|
| 1288 |
+
|
| 1289 |
+
1. Update endpoint URLs for file info
|
| 1290 |
+
2. Update error handling to use new format
|
| 1291 |
+
3. Implement webhook handlers for better UX
|
| 1292 |
+
4. Use batch operations for improved performance
|
| 1293 |
+
|
| 1294 |
+
## Changelog
|
| 1295 |
+
|
| 1296 |
+
### v1.1.0 (2024-01-15)
|
| 1297 |
+
|
| 1298 |
+
- Added webhook support
|
| 1299 |
+
- Introduced batch file operations
|
| 1300 |
+
- Enhanced error responses with details
|
| 1301 |
+
- Added signed URL generation
|
| 1302 |
+
- Improved pagination format
|
| 1303 |
+
- Added system performance endpoints
|
| 1304 |
+
|
| 1305 |
+
### v1.0.0 (2023-12-01)
|
| 1306 |
+
|
| 1307 |
+
- Initial API release
|
| 1308 |
+
- Basic CRUD operations for files
|
| 1309 |
+
- Video generation capabilities
|
| 1310 |
+
- Job management system
|
| 1311 |
+
- System monitoring endpoints
|
AUTHENTICATION_GUIDE.md
ADDED
|
@@ -0,0 +1,1120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# T2M Authentication & Session Flow Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This guide covers the complete authentication and session management flow for the T2M (Text-to-Media) application using Clerk as the authentication provider. It includes frontend SDK setup, route protection, API token management, and secure file access patterns.
|
| 6 |
+
|
| 7 |
+
## Table of Contents
|
| 8 |
+
|
| 9 |
+
1. [Clerk Setup & Configuration](#clerk-setup--configuration)
|
| 10 |
+
2. [Frontend SDK Integration](#frontend-sdk-integration)
|
| 11 |
+
3. [Route Protection Strategy](#route-protection-strategy)
|
| 12 |
+
4. [API Token Management](#api-token-management)
|
| 13 |
+
5. [Secure File Access](#secure-file-access)
|
| 14 |
+
6. [Session Management](#session-management)
|
| 15 |
+
7. [Security Best Practices](#security-best-practices)
|
| 16 |
+
8. [Implementation Examples](#implementation-examples)
|
| 17 |
+
|
| 18 |
+
## Clerk Setup & Configuration
|
| 19 |
+
|
| 20 |
+
### 1. Clerk Dashboard Configuration
|
| 21 |
+
|
| 22 |
+
**Environment Setup:**
|
| 23 |
+
- **Development**: `https://dev.t2m-app.com`
|
| 24 |
+
- **Production**: `https://t2m-app.com`
|
| 25 |
+
|
| 26 |
+
**Required Clerk Settings:**
|
| 27 |
+
```javascript
|
| 28 |
+
// Environment Variables
|
| 29 |
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
|
| 30 |
+
CLERK_SECRET_KEY=sk_test_...
|
| 31 |
+
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
| 32 |
+
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
| 33 |
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
| 34 |
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
**Clerk Application Settings:**
|
| 38 |
+
- **Session token lifetime**: 7 days
|
| 39 |
+
- **JWT template**: Custom template for API integration
|
| 40 |
+
- **Allowed origins**: Your frontend domains
|
| 41 |
+
- **Webhook endpoints**: For user lifecycle events
|
| 42 |
+
|
| 43 |
+
### 2. JWT Template Configuration
|
| 44 |
+
|
| 45 |
+
Create a custom JWT template in Clerk dashboard:
|
| 46 |
+
|
| 47 |
+
```json
|
| 48 |
+
{
|
| 49 |
+
"aud": "t2m-api",
|
| 50 |
+
"exp": "{{session.expire_at}}",
|
| 51 |
+
"iat": "{{session.created_at}}",
|
| 52 |
+
"iss": "https://clerk.t2m-app.com",
|
| 53 |
+
"sub": "{{user.id}}",
|
| 54 |
+
"user_id": "{{user.id}}",
|
| 55 |
+
"email": "{{user.primary_email_address.email_address}}",
|
| 56 |
+
"role": "{{user.public_metadata.role}}",
|
| 57 |
+
"permissions": "{{user.public_metadata.permissions}}",
|
| 58 |
+
"subscription_tier": "{{user.public_metadata.subscription_tier}}"
|
| 59 |
+
}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## Frontend SDK Integration
|
| 63 |
+
|
| 64 |
+
### 1. Next.js Setup (@clerk/nextjs)
|
| 65 |
+
|
| 66 |
+
**Installation:**
|
| 67 |
+
```bash
|
| 68 |
+
npm install @clerk/nextjs
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**App Router Configuration (app/layout.tsx):**
|
| 72 |
+
```typescript
|
| 73 |
+
import { ClerkProvider } from '@clerk/nextjs'
|
| 74 |
+
import { Inter } from 'next/font/google'
|
| 75 |
+
import './globals.css'
|
| 76 |
+
|
| 77 |
+
const inter = Inter({ subsets: ['latin'] })
|
| 78 |
+
|
| 79 |
+
export default function RootLayout({
|
| 80 |
+
children,
|
| 81 |
+
}: {
|
| 82 |
+
children: React.ReactNode
|
| 83 |
+
}) {
|
| 84 |
+
return (
|
| 85 |
+
<ClerkProvider>
|
| 86 |
+
<html lang="en">
|
| 87 |
+
<body className={inter.className}>
|
| 88 |
+
{children}
|
| 89 |
+
</body>
|
| 90 |
+
</html>
|
| 91 |
+
</ClerkProvider>
|
| 92 |
+
)
|
| 93 |
+
}
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
**Middleware Setup (middleware.ts):**
|
| 97 |
+
```typescript
|
| 98 |
+
import { authMiddleware } from "@clerk/nextjs";
|
| 99 |
+
|
| 100 |
+
export default authMiddleware({
|
| 101 |
+
// Public routes that don't require authentication
|
| 102 |
+
publicRoutes: [
|
| 103 |
+
"/",
|
| 104 |
+
"/api/auth/health",
|
| 105 |
+
"/api/system/health",
|
| 106 |
+
"/pricing",
|
| 107 |
+
"/about",
|
| 108 |
+
"/contact"
|
| 109 |
+
],
|
| 110 |
+
|
| 111 |
+
// Routes that should be ignored by Clerk
|
| 112 |
+
ignoredRoutes: [
|
| 113 |
+
"/api/webhooks/clerk",
|
| 114 |
+
"/api/files/secure/(.*)" // Signed URL access
|
| 115 |
+
],
|
| 116 |
+
|
| 117 |
+
// API routes that require authentication
|
| 118 |
+
apiRoutes: ["/api/(.*)"],
|
| 119 |
+
|
| 120 |
+
// Redirect after sign in
|
| 121 |
+
afterAuth(auth, req, evt) {
|
| 122 |
+
// Handle users who aren't authenticated
|
| 123 |
+
if (!auth.userId && !auth.isPublicRoute) {
|
| 124 |
+
return redirectToSignIn({ returnBackUrl: req.url });
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Redirect authenticated users away from public-only pages
|
| 128 |
+
if (auth.userId && auth.isPublicRoute && req.nextUrl.pathname === "/") {
|
| 129 |
+
const dashboard = new URL("/dashboard", req.url);
|
| 130 |
+
return NextResponse.redirect(dashboard);
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
export const config = {
|
| 136 |
+
matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
|
| 137 |
+
};
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### 2. React Components Setup
|
| 141 |
+
|
| 142 |
+
**Authentication Components:**
|
| 143 |
+
```typescript
|
| 144 |
+
// components/auth/SignInButton.tsx
|
| 145 |
+
import { SignInButton as ClerkSignInButton } from "@clerk/nextjs";
|
| 146 |
+
|
| 147 |
+
export function SignInButton() {
|
| 148 |
+
return (
|
| 149 |
+
<ClerkSignInButton mode="modal">
|
| 150 |
+
<button className="btn-primary">
|
| 151 |
+
Sign In
|
| 152 |
+
</button>
|
| 153 |
+
</ClerkSignInButton>
|
| 154 |
+
);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// components/auth/UserButton.tsx
|
| 158 |
+
import { UserButton as ClerkUserButton } from "@clerk/nextjs";
|
| 159 |
+
|
| 160 |
+
export function UserButton() {
|
| 161 |
+
return (
|
| 162 |
+
<ClerkUserButton
|
| 163 |
+
afterSignOutUrl="/"
|
| 164 |
+
appearance={{
|
| 165 |
+
elements: {
|
| 166 |
+
avatarBox: "w-8 h-8"
|
| 167 |
+
}
|
| 168 |
+
}}
|
| 169 |
+
/>
|
| 170 |
+
);
|
| 171 |
+
}
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
**Protected Page Component:**
|
| 175 |
+
```typescript
|
| 176 |
+
// components/auth/ProtectedRoute.tsx
|
| 177 |
+
import { useAuth } from "@clerk/nextjs";
|
| 178 |
+
import { useRouter } from "next/navigation";
|
| 179 |
+
import { useEffect } from "react";
|
| 180 |
+
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
| 181 |
+
|
| 182 |
+
interface ProtectedRouteProps {
|
| 183 |
+
children: React.ReactNode;
|
| 184 |
+
requiredRole?: string;
|
| 185 |
+
requiredPermissions?: string[];
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
export function ProtectedRoute({
|
| 189 |
+
children,
|
| 190 |
+
requiredRole,
|
| 191 |
+
requiredPermissions
|
| 192 |
+
}: ProtectedRouteProps) {
|
| 193 |
+
const { isLoaded, isSignedIn, user } = useAuth();
|
| 194 |
+
const router = useRouter();
|
| 195 |
+
|
| 196 |
+
useEffect(() => {
|
| 197 |
+
if (isLoaded && !isSignedIn) {
|
| 198 |
+
router.push("/sign-in");
|
| 199 |
+
}
|
| 200 |
+
}, [isLoaded, isSignedIn, router]);
|
| 201 |
+
|
| 202 |
+
if (!isLoaded) {
|
| 203 |
+
return <LoadingSpinner />;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
if (!isSignedIn) {
|
| 207 |
+
return null;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Check role-based access
|
| 211 |
+
if (requiredRole) {
|
| 212 |
+
const userRole = user?.publicMetadata?.role as string;
|
| 213 |
+
if (userRole !== requiredRole) {
|
| 214 |
+
return <div>Access denied. Required role: {requiredRole}</div>;
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Check permission-based access
|
| 219 |
+
if (requiredPermissions) {
|
| 220 |
+
const userPermissions = user?.publicMetadata?.permissions as string[] || [];
|
| 221 |
+
const hasPermission = requiredPermissions.every(permission =>
|
| 222 |
+
userPermissions.includes(permission)
|
| 223 |
+
);
|
| 224 |
+
|
| 225 |
+
if (!hasPermission) {
|
| 226 |
+
return <div>Access denied. Missing required permissions.</div>;
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
return <>{children}</>;
|
| 231 |
+
}
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
## Route Protection Strategy
|
| 235 |
+
|
| 236 |
+
### 1. Public Routes (No Authentication Required)
|
| 237 |
+
|
| 238 |
+
```typescript
|
| 239 |
+
// Public routes configuration
|
| 240 |
+
const PUBLIC_ROUTES = [
|
| 241 |
+
"/", // Landing page
|
| 242 |
+
"/pricing", // Pricing information
|
| 243 |
+
"/about", // About page
|
| 244 |
+
"/contact", // Contact page
|
| 245 |
+
"/api/auth/health", // Auth service health
|
| 246 |
+
"/api/system/health", // System health check
|
| 247 |
+
"/legal/privacy", // Privacy policy
|
| 248 |
+
"/legal/terms" // Terms of service
|
| 249 |
+
];
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
### 2. Protected Routes (Authentication Required)
|
| 253 |
+
|
| 254 |
+
```typescript
|
| 255 |
+
// Protected routes with different access levels
|
| 256 |
+
const PROTECTED_ROUTES = {
|
| 257 |
+
// Basic authenticated routes
|
| 258 |
+
AUTHENTICATED: [
|
| 259 |
+
"/dashboard",
|
| 260 |
+
"/profile",
|
| 261 |
+
"/files",
|
| 262 |
+
"/videos",
|
| 263 |
+
"/jobs"
|
| 264 |
+
],
|
| 265 |
+
|
| 266 |
+
// Admin-only routes
|
| 267 |
+
ADMIN: [
|
| 268 |
+
"/admin",
|
| 269 |
+
"/admin/users",
|
| 270 |
+
"/admin/system",
|
| 271 |
+
"/admin/analytics"
|
| 272 |
+
],
|
| 273 |
+
|
| 274 |
+
// Premium subscription routes
|
| 275 |
+
PREMIUM: [
|
| 276 |
+
"/premium/advanced-generation",
|
| 277 |
+
"/premium/batch-processing",
|
| 278 |
+
"/premium/priority-queue"
|
| 279 |
+
]
|
| 280 |
+
};
|
| 281 |
+
```
|
| 282 |
+
|
| 283 |
+
### 3. API Route Protection
|
| 284 |
+
|
| 285 |
+
```typescript
|
| 286 |
+
// app/api/auth/route-protection.ts
|
| 287 |
+
import { auth } from "@clerk/nextjs";
|
| 288 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 289 |
+
|
| 290 |
+
export async function requireAuth(request: NextRequest) {
|
| 291 |
+
const { userId } = auth();
|
| 292 |
+
|
| 293 |
+
if (!userId) {
|
| 294 |
+
return NextResponse.json(
|
| 295 |
+
{ error: "Unauthorized" },
|
| 296 |
+
{ status: 401 }
|
| 297 |
+
);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
return userId;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
export async function requireRole(request: NextRequest, requiredRole: string) {
|
| 304 |
+
const { userId, sessionClaims } = auth();
|
| 305 |
+
|
| 306 |
+
if (!userId) {
|
| 307 |
+
return NextResponse.json(
|
| 308 |
+
{ error: "Unauthorized" },
|
| 309 |
+
{ status: 401 }
|
| 310 |
+
);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
const userRole = sessionClaims?.metadata?.role as string;
|
| 314 |
+
|
| 315 |
+
if (userRole !== requiredRole) {
|
| 316 |
+
return NextResponse.json(
|
| 317 |
+
{ error: "Forbidden" },
|
| 318 |
+
{ status: 403 }
|
| 319 |
+
);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
return userId;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// Usage in API routes
|
| 326 |
+
// app/api/files/route.ts
|
| 327 |
+
import { requireAuth } from "@/app/api/auth/route-protection";
|
| 328 |
+
|
| 329 |
+
export async function GET(request: NextRequest) {
|
| 330 |
+
const userId = await requireAuth(request);
|
| 331 |
+
if (userId instanceof NextResponse) return userId; // Error response
|
| 332 |
+
|
| 333 |
+
// Continue with authenticated logic
|
| 334 |
+
// ...
|
| 335 |
+
}
|
| 336 |
+
```
|
| 337 |
+
|
| 338 |
+
## API Token Management
|
| 339 |
+
|
| 340 |
+
### 1. Token Retrieval in Frontend
|
| 341 |
+
|
| 342 |
+
```typescript
|
| 343 |
+
// hooks/useApiToken.ts
|
| 344 |
+
import { useAuth } from "@clerk/nextjs";
|
| 345 |
+
import { useCallback } from "react";
|
| 346 |
+
|
| 347 |
+
export function useApiToken() {
|
| 348 |
+
const { getToken } = useAuth();
|
| 349 |
+
|
| 350 |
+
const getApiToken = useCallback(async () => {
|
| 351 |
+
try {
|
| 352 |
+
// Get token with custom JWT template
|
| 353 |
+
const token = await getToken({ template: "t2m-api" });
|
| 354 |
+
return token;
|
| 355 |
+
} catch (error) {
|
| 356 |
+
console.error("Failed to get API token:", error);
|
| 357 |
+
throw new Error("Authentication failed");
|
| 358 |
+
}
|
| 359 |
+
}, [getToken]);
|
| 360 |
+
|
| 361 |
+
return { getApiToken };
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Usage in components
|
| 365 |
+
function VideoUpload() {
|
| 366 |
+
const { getApiToken } = useApiToken();
|
| 367 |
+
|
| 368 |
+
const uploadVideo = async (file: File) => {
|
| 369 |
+
const token = await getApiToken();
|
| 370 |
+
|
| 371 |
+
const formData = new FormData();
|
| 372 |
+
formData.append('file', file);
|
| 373 |
+
|
| 374 |
+
const response = await fetch('/api/files/upload', {
|
| 375 |
+
method: 'POST',
|
| 376 |
+
headers: {
|
| 377 |
+
'Authorization': `Bearer ${token}`
|
| 378 |
+
},
|
| 379 |
+
body: formData
|
| 380 |
+
});
|
| 381 |
+
|
| 382 |
+
return response.json();
|
| 383 |
+
};
|
| 384 |
+
|
| 385 |
+
// ...
|
| 386 |
+
}
|
| 387 |
+
```
|
| 388 |
+
|
| 389 |
+
### 2. API Client with Automatic Token Management
|
| 390 |
+
|
| 391 |
+
```typescript
|
| 392 |
+
// lib/api-client.ts
|
| 393 |
+
import { useAuth } from "@clerk/nextjs";
|
| 394 |
+
|
| 395 |
+
class ApiClient {
|
| 396 |
+
private baseUrl: string;
|
| 397 |
+
private getToken: () => Promise<string | null>;
|
| 398 |
+
|
| 399 |
+
constructor(baseUrl: string, getToken: () => Promise<string | null>) {
|
| 400 |
+
this.baseUrl = baseUrl;
|
| 401 |
+
this.getToken = getToken;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
private async request<T>(
|
| 405 |
+
endpoint: string,
|
| 406 |
+
options: RequestInit = {}
|
| 407 |
+
): Promise<T> {
|
| 408 |
+
const token = await this.getToken();
|
| 409 |
+
|
| 410 |
+
const config: RequestInit = {
|
| 411 |
+
...options,
|
| 412 |
+
headers: {
|
| 413 |
+
'Content-Type': 'application/json',
|
| 414 |
+
...(token && { 'Authorization': `Bearer ${token}` }),
|
| 415 |
+
...options.headers,
|
| 416 |
+
},
|
| 417 |
+
};
|
| 418 |
+
|
| 419 |
+
const response = await fetch(`${this.baseUrl}${endpoint}`, config);
|
| 420 |
+
|
| 421 |
+
if (!response.ok) {
|
| 422 |
+
const error = await response.json();
|
| 423 |
+
throw new Error(error.message || 'API request failed');
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
return response.json();
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
// File operations
|
| 430 |
+
async uploadFile(file: File, metadata?: any) {
|
| 431 |
+
const formData = new FormData();
|
| 432 |
+
formData.append('file', file);
|
| 433 |
+
if (metadata) {
|
| 434 |
+
Object.entries(metadata).forEach(([key, value]) => {
|
| 435 |
+
formData.append(key, value as string);
|
| 436 |
+
});
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
const token = await this.getToken();
|
| 440 |
+
return fetch(`${this.baseUrl}/files/upload`, {
|
| 441 |
+
method: 'POST',
|
| 442 |
+
headers: {
|
| 443 |
+
...(token && { 'Authorization': `Bearer ${token}` })
|
| 444 |
+
},
|
| 445 |
+
body: formData
|
| 446 |
+
}).then(res => res.json());
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
async getFiles(params?: any) {
|
| 450 |
+
const query = params ? `?${new URLSearchParams(params)}` : '';
|
| 451 |
+
return this.request(`/files${query}`);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
// Video operations
|
| 455 |
+
async generateVideo(prompt: string, options?: any) {
|
| 456 |
+
return this.request('/videos/generate', {
|
| 457 |
+
method: 'POST',
|
| 458 |
+
body: JSON.stringify({ prompt, ...options })
|
| 459 |
+
});
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
async getJobStatus(jobId: string) {
|
| 463 |
+
return this.request(`/jobs/${jobId}`);
|
| 464 |
+
}
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
// Hook for using API client
|
| 468 |
+
export function useApiClient() {
|
| 469 |
+
const { getToken } = useAuth();
|
| 470 |
+
|
| 471 |
+
const apiClient = new ApiClient(
|
| 472 |
+
process.env.NEXT_PUBLIC_API_URL || '/api/v1',
|
| 473 |
+
() => getToken({ template: "t2m-api" })
|
| 474 |
+
);
|
| 475 |
+
|
| 476 |
+
return apiClient;
|
| 477 |
+
}
|
| 478 |
+
```
|
| 479 |
+
|
| 480 |
+
### 3. Backend Token Validation
|
| 481 |
+
|
| 482 |
+
```typescript
|
| 483 |
+
// Backend API token validation (if using proxy)
|
| 484 |
+
// app/api/auth/validate-token.ts
|
| 485 |
+
import { verifyToken } from "@clerk/backend";
|
| 486 |
+
|
| 487 |
+
export async function validateClerkToken(token: string) {
|
| 488 |
+
try {
|
| 489 |
+
const payload = await verifyToken(token, {
|
| 490 |
+
jwtKey: process.env.CLERK_JWT_KEY,
|
| 491 |
+
authorizedParties: [process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY]
|
| 492 |
+
});
|
| 493 |
+
|
| 494 |
+
return {
|
| 495 |
+
userId: payload.sub,
|
| 496 |
+
email: payload.email,
|
| 497 |
+
role: payload.role,
|
| 498 |
+
permissions: payload.permissions,
|
| 499 |
+
subscriptionTier: payload.subscription_tier
|
| 500 |
+
};
|
| 501 |
+
} catch (error) {
|
| 502 |
+
throw new Error('Invalid token');
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
// Usage in API routes
|
| 507 |
+
export async function POST(request: NextRequest) {
|
| 508 |
+
const authHeader = request.headers.get('authorization');
|
| 509 |
+
const token = authHeader?.replace('Bearer ', '');
|
| 510 |
+
|
| 511 |
+
if (!token) {
|
| 512 |
+
return NextResponse.json({ error: 'No token provided' }, { status: 401 });
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
try {
|
| 516 |
+
const user = await validateClerkToken(token);
|
| 517 |
+
// Continue with authenticated logic
|
| 518 |
+
} catch (error) {
|
| 519 |
+
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
| 520 |
+
}
|
| 521 |
+
}
|
| 522 |
+
```
|
| 523 |
+
|
| 524 |
+
## Secure File Access
|
| 525 |
+
|
| 526 |
+
### 1. Signed URL Generation
|
| 527 |
+
|
| 528 |
+
```typescript
|
| 529 |
+
// Backend: Generate signed URLs for secure file access
|
| 530 |
+
// app/api/files/[fileId]/signed-url/route.ts
|
| 531 |
+
import { auth } from "@clerk/nextjs";
|
| 532 |
+
import { createHmac } from "crypto";
|
| 533 |
+
|
| 534 |
+
export async function POST(
|
| 535 |
+
request: NextRequest,
|
| 536 |
+
{ params }: { params: { fileId: string } }
|
| 537 |
+
) {
|
| 538 |
+
const { userId } = auth();
|
| 539 |
+
if (!userId) {
|
| 540 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
const { fileId } = params;
|
| 544 |
+
const { expiresIn = 3600 } = await request.json(); // Default 1 hour
|
| 545 |
+
|
| 546 |
+
// Verify user owns the file
|
| 547 |
+
const file = await getFileById(fileId);
|
| 548 |
+
if (!file || file.userId !== userId) {
|
| 549 |
+
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
// Generate signed URL
|
| 553 |
+
const expires = Math.floor(Date.now() / 1000) + expiresIn;
|
| 554 |
+
const signature = createHmac('sha256', process.env.FILE_SIGNING_SECRET!)
|
| 555 |
+
.update(`${fileId}:${userId}:${expires}`)
|
| 556 |
+
.digest('hex');
|
| 557 |
+
|
| 558 |
+
const signedUrl = `${process.env.NEXT_PUBLIC_API_URL}/files/secure/${fileId}?` +
|
| 559 |
+
`user_id=${userId}&expires=${expires}&signature=${signature}`;
|
| 560 |
+
|
| 561 |
+
return NextResponse.json({
|
| 562 |
+
success: true,
|
| 563 |
+
data: {
|
| 564 |
+
url: signedUrl,
|
| 565 |
+
expires_at: new Date(expires * 1000).toISOString()
|
| 566 |
+
}
|
| 567 |
+
});
|
| 568 |
+
}
|
| 569 |
+
```
|
| 570 |
+
|
| 571 |
+
### 2. Signed URL Validation
|
| 572 |
+
|
| 573 |
+
```typescript
|
| 574 |
+
// Backend: Validate signed URLs
|
| 575 |
+
// app/api/files/secure/[fileId]/route.ts
|
| 576 |
+
import { createHmac } from "crypto";
|
| 577 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 578 |
+
|
| 579 |
+
export async function GET(
|
| 580 |
+
request: NextRequest,
|
| 581 |
+
{ params }: { params: { fileId: string } }
|
| 582 |
+
) {
|
| 583 |
+
const { fileId } = params;
|
| 584 |
+
const { searchParams } = new URL(request.url);
|
| 585 |
+
|
| 586 |
+
const userId = searchParams.get('user_id');
|
| 587 |
+
const expires = searchParams.get('expires');
|
| 588 |
+
const signature = searchParams.get('signature');
|
| 589 |
+
|
| 590 |
+
if (!userId || !expires || !signature) {
|
| 591 |
+
return NextResponse.json({ error: "Invalid signed URL" }, { status: 400 });
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
// Check expiration
|
| 595 |
+
const expiresTimestamp = parseInt(expires);
|
| 596 |
+
if (Date.now() / 1000 > expiresTimestamp) {
|
| 597 |
+
return NextResponse.json({ error: "Signed URL expired" }, { status: 410 });
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
// Verify signature
|
| 601 |
+
const expectedSignature = createHmac('sha256', process.env.FILE_SIGNING_SECRET!)
|
| 602 |
+
.update(`${fileId}:${userId}:${expires}`)
|
| 603 |
+
.digest('hex');
|
| 604 |
+
|
| 605 |
+
if (signature !== expectedSignature) {
|
| 606 |
+
return NextResponse.json({ error: "Invalid signature" }, { status: 403 });
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// Serve file
|
| 610 |
+
const file = await getFileById(fileId);
|
| 611 |
+
if (!file || file.userId !== userId) {
|
| 612 |
+
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// Return file stream
|
| 616 |
+
const fileStream = await getFileStream(file.path);
|
| 617 |
+
return new NextResponse(fileStream, {
|
| 618 |
+
headers: {
|
| 619 |
+
'Content-Type': file.contentType,
|
| 620 |
+
'Content-Disposition': `attachment; filename="${file.filename}"`,
|
| 621 |
+
'Cache-Control': 'private, max-age=3600'
|
| 622 |
+
}
|
| 623 |
+
});
|
| 624 |
+
}
|
| 625 |
+
```
|
| 626 |
+
|
| 627 |
+
### 3. Frontend: Secure Video Player
|
| 628 |
+
|
| 629 |
+
```typescript
|
| 630 |
+
// components/VideoPlayer.tsx
|
| 631 |
+
import { useApiClient } from "@/lib/api-client";
|
| 632 |
+
import { useEffect, useState } from "react";
|
| 633 |
+
|
| 634 |
+
interface VideoPlayerProps {
|
| 635 |
+
jobId: string;
|
| 636 |
+
autoplay?: boolean;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
export function VideoPlayer({ jobId, autoplay = false }: VideoPlayerProps) {
|
| 640 |
+
const [signedUrl, setSignedUrl] = useState<string | null>(null);
|
| 641 |
+
const [loading, setLoading] = useState(true);
|
| 642 |
+
const [error, setError] = useState<string | null>(null);
|
| 643 |
+
const apiClient = useApiClient();
|
| 644 |
+
|
| 645 |
+
useEffect(() => {
|
| 646 |
+
async function getSignedUrl() {
|
| 647 |
+
try {
|
| 648 |
+
setLoading(true);
|
| 649 |
+
|
| 650 |
+
// Get signed URL for video
|
| 651 |
+
const response = await apiClient.request(`/videos/${jobId}/signed-url`, {
|
| 652 |
+
method: 'POST',
|
| 653 |
+
body: JSON.stringify({ expiresIn: 3600 }) // 1 hour
|
| 654 |
+
});
|
| 655 |
+
|
| 656 |
+
setSignedUrl(response.data.url);
|
| 657 |
+
} catch (err) {
|
| 658 |
+
setError(err instanceof Error ? err.message : 'Failed to load video');
|
| 659 |
+
} finally {
|
| 660 |
+
setLoading(false);
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
getSignedUrl();
|
| 665 |
+
}, [jobId, apiClient]);
|
| 666 |
+
|
| 667 |
+
if (loading) return <div>Loading video...</div>;
|
| 668 |
+
if (error) return <div>Error: {error}</div>;
|
| 669 |
+
if (!signedUrl) return <div>Video not available</div>;
|
| 670 |
+
|
| 671 |
+
return (
|
| 672 |
+
<video
|
| 673 |
+
src={signedUrl}
|
| 674 |
+
controls
|
| 675 |
+
autoPlay={autoplay}
|
| 676 |
+
className="w-full h-auto rounded-lg"
|
| 677 |
+
onError={() => setError('Failed to load video')}
|
| 678 |
+
>
|
| 679 |
+
Your browser does not support the video tag.
|
| 680 |
+
</video>
|
| 681 |
+
);
|
| 682 |
+
}
|
| 683 |
+
```
|
| 684 |
+
|
| 685 |
+
## Session Management
|
| 686 |
+
|
| 687 |
+
### 1. Session Configuration
|
| 688 |
+
|
| 689 |
+
```typescript
|
| 690 |
+
// lib/session-config.ts
|
| 691 |
+
export const SESSION_CONFIG = {
|
| 692 |
+
// Session duration
|
| 693 |
+
maxAge: 7 * 24 * 60 * 60, // 7 days in seconds
|
| 694 |
+
|
| 695 |
+
// Token refresh threshold
|
| 696 |
+
refreshThreshold: 5 * 60, // Refresh if expires in 5 minutes
|
| 697 |
+
|
| 698 |
+
// Automatic logout on inactivity
|
| 699 |
+
inactivityTimeout: 30 * 60, // 30 minutes
|
| 700 |
+
|
| 701 |
+
// Remember me option
|
| 702 |
+
rememberMe: {
|
| 703 |
+
enabled: true,
|
| 704 |
+
duration: 30 * 24 * 60 * 60 // 30 days
|
| 705 |
+
}
|
| 706 |
+
};
|
| 707 |
+
```
|
| 708 |
+
|
| 709 |
+
### 2. Session Monitoring Hook
|
| 710 |
+
|
| 711 |
+
```typescript
|
| 712 |
+
// hooks/useSessionMonitor.ts
|
| 713 |
+
import { useAuth } from "@clerk/nextjs";
|
| 714 |
+
import { useEffect, useRef } from "react";
|
| 715 |
+
|
| 716 |
+
export function useSessionMonitor() {
|
| 717 |
+
const { isSignedIn, signOut } = useAuth();
|
| 718 |
+
const lastActivityRef = useRef(Date.now());
|
| 719 |
+
const inactivityTimerRef = useRef<NodeJS.Timeout>();
|
| 720 |
+
|
| 721 |
+
const resetInactivityTimer = () => {
|
| 722 |
+
lastActivityRef.current = Date.now();
|
| 723 |
+
|
| 724 |
+
if (inactivityTimerRef.current) {
|
| 725 |
+
clearTimeout(inactivityTimerRef.current);
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
inactivityTimerRef.current = setTimeout(() => {
|
| 729 |
+
if (isSignedIn) {
|
| 730 |
+
signOut();
|
| 731 |
+
alert('You have been logged out due to inactivity.');
|
| 732 |
+
}
|
| 733 |
+
}, SESSION_CONFIG.inactivityTimeout * 1000);
|
| 734 |
+
};
|
| 735 |
+
|
| 736 |
+
useEffect(() => {
|
| 737 |
+
if (!isSignedIn) return;
|
| 738 |
+
|
| 739 |
+
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
|
| 740 |
+
|
| 741 |
+
events.forEach(event => {
|
| 742 |
+
document.addEventListener(event, resetInactivityTimer, true);
|
| 743 |
+
});
|
| 744 |
+
|
| 745 |
+
resetInactivityTimer(); // Initialize timer
|
| 746 |
+
|
| 747 |
+
return () => {
|
| 748 |
+
events.forEach(event => {
|
| 749 |
+
document.removeEventListener(event, resetInactivityTimer, true);
|
| 750 |
+
});
|
| 751 |
+
|
| 752 |
+
if (inactivityTimerRef.current) {
|
| 753 |
+
clearTimeout(inactivityTimerRef.current);
|
| 754 |
+
}
|
| 755 |
+
};
|
| 756 |
+
}, [isSignedIn]);
|
| 757 |
+
}
|
| 758 |
+
```
|
| 759 |
+
|
| 760 |
+
### 3. Token Refresh Management
|
| 761 |
+
|
| 762 |
+
```typescript
|
| 763 |
+
// hooks/useTokenRefresh.ts
|
| 764 |
+
import { useAuth } from "@clerk/nextjs";
|
| 765 |
+
import { useEffect, useCallback } from "react";
|
| 766 |
+
|
| 767 |
+
export function useTokenRefresh() {
|
| 768 |
+
const { getToken, isSignedIn } = useAuth();
|
| 769 |
+
|
| 770 |
+
const checkTokenExpiry = useCallback(async () => {
|
| 771 |
+
if (!isSignedIn) return;
|
| 772 |
+
|
| 773 |
+
try {
|
| 774 |
+
const token = await getToken({ template: "t2m-api" });
|
| 775 |
+
if (!token) return;
|
| 776 |
+
|
| 777 |
+
// Decode JWT to check expiry
|
| 778 |
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
| 779 |
+
const expiryTime = payload.exp * 1000; // Convert to milliseconds
|
| 780 |
+
const currentTime = Date.now();
|
| 781 |
+
const timeUntilExpiry = expiryTime - currentTime;
|
| 782 |
+
|
| 783 |
+
// Refresh if token expires within threshold
|
| 784 |
+
if (timeUntilExpiry < SESSION_CONFIG.refreshThreshold * 1000) {
|
| 785 |
+
await getToken({ template: "t2m-api", skipCache: true });
|
| 786 |
+
}
|
| 787 |
+
} catch (error) {
|
| 788 |
+
console.error('Token refresh failed:', error);
|
| 789 |
+
}
|
| 790 |
+
}, [getToken, isSignedIn]);
|
| 791 |
+
|
| 792 |
+
useEffect(() => {
|
| 793 |
+
if (!isSignedIn) return;
|
| 794 |
+
|
| 795 |
+
// Check token expiry every minute
|
| 796 |
+
const interval = setInterval(checkTokenExpiry, 60 * 1000);
|
| 797 |
+
|
| 798 |
+
return () => clearInterval(interval);
|
| 799 |
+
}, [isSignedIn, checkTokenExpiry]);
|
| 800 |
+
}
|
| 801 |
+
```
|
| 802 |
+
|
| 803 |
+
## Security Best Practices
|
| 804 |
+
|
| 805 |
+
### 1. Token Security
|
| 806 |
+
|
| 807 |
+
```typescript
|
| 808 |
+
// Security guidelines for token handling
|
| 809 |
+
|
| 810 |
+
// ✅ DO: Use secure token storage
|
| 811 |
+
const { getToken } = useAuth();
|
| 812 |
+
const token = await getToken({ template: "t2m-api" });
|
| 813 |
+
|
| 814 |
+
// ❌ DON'T: Store tokens in localStorage or sessionStorage
|
| 815 |
+
localStorage.setItem('token', token); // NEVER DO THIS
|
| 816 |
+
|
| 817 |
+
// ✅ DO: Use tokens for API calls only
|
| 818 |
+
const response = await fetch('/api/files', {
|
| 819 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 820 |
+
});
|
| 821 |
+
|
| 822 |
+
// ❌ DON'T: Embed tokens in URLs or HTML
|
| 823 |
+
const videoUrl = `/video?token=${token}`; // NEVER DO THIS
|
| 824 |
+
```
|
| 825 |
+
|
| 826 |
+
### 2. CSRF Protection
|
| 827 |
+
|
| 828 |
+
```typescript
|
| 829 |
+
// middleware.ts - CSRF protection
|
| 830 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 831 |
+
|
| 832 |
+
export function middleware(request: NextRequest) {
|
| 833 |
+
// CSRF protection for state-changing operations
|
| 834 |
+
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) {
|
| 835 |
+
const origin = request.headers.get('origin');
|
| 836 |
+
const host = request.headers.get('host');
|
| 837 |
+
|
| 838 |
+
if (origin && !origin.includes(host!)) {
|
| 839 |
+
return NextResponse.json(
|
| 840 |
+
{ error: 'CSRF protection: Invalid origin' },
|
| 841 |
+
{ status: 403 }
|
| 842 |
+
);
|
| 843 |
+
}
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
return NextResponse.next();
|
| 847 |
+
}
|
| 848 |
+
```
|
| 849 |
+
|
| 850 |
+
### 3. Rate Limiting
|
| 851 |
+
|
| 852 |
+
```typescript
|
| 853 |
+
// lib/rate-limiter.ts
|
| 854 |
+
import { Ratelimit } from "@upstash/ratelimit";
|
| 855 |
+
import { Redis } from "@upstash/redis";
|
| 856 |
+
|
| 857 |
+
const redis = new Redis({
|
| 858 |
+
url: process.env.UPSTASH_REDIS_REST_URL!,
|
| 859 |
+
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
| 860 |
+
});
|
| 861 |
+
|
| 862 |
+
export const rateLimiter = new Ratelimit({
|
| 863 |
+
redis,
|
| 864 |
+
limiter: Ratelimit.slidingWindow(100, "1 h"), // 100 requests per hour
|
| 865 |
+
analytics: true,
|
| 866 |
+
});
|
| 867 |
+
|
| 868 |
+
// Usage in API routes
|
| 869 |
+
export async function POST(request: NextRequest) {
|
| 870 |
+
const { userId } = auth();
|
| 871 |
+
if (!userId) {
|
| 872 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
const { success, limit, reset, remaining } = await rateLimiter.limit(userId);
|
| 876 |
+
|
| 877 |
+
if (!success) {
|
| 878 |
+
return NextResponse.json(
|
| 879 |
+
{ error: "Rate limit exceeded" },
|
| 880 |
+
{
|
| 881 |
+
status: 429,
|
| 882 |
+
headers: {
|
| 883 |
+
'X-RateLimit-Limit': limit.toString(),
|
| 884 |
+
'X-RateLimit-Remaining': remaining.toString(),
|
| 885 |
+
'X-RateLimit-Reset': reset.toString(),
|
| 886 |
+
}
|
| 887 |
+
}
|
| 888 |
+
);
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
// Continue with request processing
|
| 892 |
+
}
|
| 893 |
+
```
|
| 894 |
+
|
| 895 |
+
## Implementation Examples
|
| 896 |
+
|
| 897 |
+
### 1. Complete Authentication Flow
|
| 898 |
+
|
| 899 |
+
```typescript
|
| 900 |
+
// app/dashboard/page.tsx
|
| 901 |
+
import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
|
| 902 |
+
import { useSessionMonitor } from "@/hooks/useSessionMonitor";
|
| 903 |
+
import { useTokenRefresh } from "@/hooks/useTokenRefresh";
|
| 904 |
+
|
| 905 |
+
export default function DashboardPage() {
|
| 906 |
+
useSessionMonitor(); // Monitor for inactivity
|
| 907 |
+
useTokenRefresh(); // Handle token refresh
|
| 908 |
+
|
| 909 |
+
return (
|
| 910 |
+
<ProtectedRoute>
|
| 911 |
+
<div className="dashboard">
|
| 912 |
+
<h1>Welcome to T2M Dashboard</h1>
|
| 913 |
+
{/* Dashboard content */}
|
| 914 |
+
</div>
|
| 915 |
+
</ProtectedRoute>
|
| 916 |
+
);
|
| 917 |
+
}
|
| 918 |
+
```
|
| 919 |
+
|
| 920 |
+
### 2. File Upload with Progress
|
| 921 |
+
|
| 922 |
+
```typescript
|
| 923 |
+
// components/FileUpload.tsx
|
| 924 |
+
import { useApiClient } from "@/lib/api-client";
|
| 925 |
+
import { useState } from "react";
|
| 926 |
+
|
| 927 |
+
export function FileUpload() {
|
| 928 |
+
const [uploading, setUploading] = useState(false);
|
| 929 |
+
const [progress, setProgress] = useState(0);
|
| 930 |
+
const apiClient = useApiClient();
|
| 931 |
+
|
| 932 |
+
const handleUpload = async (file: File) => {
|
| 933 |
+
setUploading(true);
|
| 934 |
+
setProgress(0);
|
| 935 |
+
|
| 936 |
+
try {
|
| 937 |
+
// Create XMLHttpRequest for progress tracking
|
| 938 |
+
const formData = new FormData();
|
| 939 |
+
formData.append('file', file);
|
| 940 |
+
|
| 941 |
+
const token = await apiClient.getApiToken();
|
| 942 |
+
|
| 943 |
+
const xhr = new XMLHttpRequest();
|
| 944 |
+
|
| 945 |
+
xhr.upload.addEventListener('progress', (e) => {
|
| 946 |
+
if (e.lengthComputable) {
|
| 947 |
+
setProgress((e.loaded / e.total) * 100);
|
| 948 |
+
}
|
| 949 |
+
});
|
| 950 |
+
|
| 951 |
+
xhr.addEventListener('load', () => {
|
| 952 |
+
if (xhr.status === 200) {
|
| 953 |
+
const response = JSON.parse(xhr.responseText);
|
| 954 |
+
console.log('Upload successful:', response);
|
| 955 |
+
}
|
| 956 |
+
});
|
| 957 |
+
|
| 958 |
+
xhr.open('POST', '/api/files/upload');
|
| 959 |
+
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
| 960 |
+
xhr.send(formData);
|
| 961 |
+
|
| 962 |
+
} catch (error) {
|
| 963 |
+
console.error('Upload failed:', error);
|
| 964 |
+
} finally {
|
| 965 |
+
setUploading(false);
|
| 966 |
+
}
|
| 967 |
+
};
|
| 968 |
+
|
| 969 |
+
return (
|
| 970 |
+
<div className="file-upload">
|
| 971 |
+
<input
|
| 972 |
+
type="file"
|
| 973 |
+
onChange={(e) => {
|
| 974 |
+
const file = e.target.files?.[0];
|
| 975 |
+
if (file) handleUpload(file);
|
| 976 |
+
}}
|
| 977 |
+
disabled={uploading}
|
| 978 |
+
/>
|
| 979 |
+
|
| 980 |
+
{uploading && (
|
| 981 |
+
<div className="progress-bar">
|
| 982 |
+
<div
|
| 983 |
+
className="progress-fill"
|
| 984 |
+
style={{ width: `${progress}%` }}
|
| 985 |
+
/>
|
| 986 |
+
<span>{Math.round(progress)}%</span>
|
| 987 |
+
</div>
|
| 988 |
+
)}
|
| 989 |
+
</div>
|
| 990 |
+
);
|
| 991 |
+
}
|
| 992 |
+
```
|
| 993 |
+
|
| 994 |
+
### 3. Video Generation with Real-time Updates
|
| 995 |
+
|
| 996 |
+
```typescript
|
| 997 |
+
// components/VideoGenerator.tsx
|
| 998 |
+
import { useApiClient } from "@/lib/api-client";
|
| 999 |
+
import { useState, useEffect } from "react";
|
| 1000 |
+
|
| 1001 |
+
export function VideoGenerator() {
|
| 1002 |
+
const [prompt, setPrompt] = useState("");
|
| 1003 |
+
const [jobId, setJobId] = useState<string | null>(null);
|
| 1004 |
+
const [status, setStatus] = useState<string>("idle");
|
| 1005 |
+
const [progress, setProgress] = useState(0);
|
| 1006 |
+
const apiClient = useApiClient();
|
| 1007 |
+
|
| 1008 |
+
const generateVideo = async () => {
|
| 1009 |
+
try {
|
| 1010 |
+
setStatus("starting");
|
| 1011 |
+
|
| 1012 |
+
const response = await apiClient.generateVideo(prompt, {
|
| 1013 |
+
duration: 10,
|
| 1014 |
+
quality: "1080p"
|
| 1015 |
+
});
|
| 1016 |
+
|
| 1017 |
+
setJobId(response.data.job_id);
|
| 1018 |
+
setStatus("processing");
|
| 1019 |
+
} catch (error) {
|
| 1020 |
+
console.error('Video generation failed:', error);
|
| 1021 |
+
setStatus("error");
|
| 1022 |
+
}
|
| 1023 |
+
};
|
| 1024 |
+
|
| 1025 |
+
// Poll for job status
|
| 1026 |
+
useEffect(() => {
|
| 1027 |
+
if (!jobId || status === "completed" || status === "error") return;
|
| 1028 |
+
|
| 1029 |
+
const pollStatus = async () => {
|
| 1030 |
+
try {
|
| 1031 |
+
const response = await apiClient.getJobStatus(jobId);
|
| 1032 |
+
setStatus(response.data.status);
|
| 1033 |
+
setProgress(response.data.progress || 0);
|
| 1034 |
+
} catch (error) {
|
| 1035 |
+
console.error('Status check failed:', error);
|
| 1036 |
+
}
|
| 1037 |
+
};
|
| 1038 |
+
|
| 1039 |
+
const interval = setInterval(pollStatus, 2000); // Poll every 2 seconds
|
| 1040 |
+
return () => clearInterval(interval);
|
| 1041 |
+
}, [jobId, status, apiClient]);
|
| 1042 |
+
|
| 1043 |
+
return (
|
| 1044 |
+
<div className="video-generator">
|
| 1045 |
+
<textarea
|
| 1046 |
+
value={prompt}
|
| 1047 |
+
onChange={(e) => setPrompt(e.target.value)}
|
| 1048 |
+
placeholder="Describe your video..."
|
| 1049 |
+
disabled={status === "processing"}
|
| 1050 |
+
/>
|
| 1051 |
+
|
| 1052 |
+
<button
|
| 1053 |
+
onClick={generateVideo}
|
| 1054 |
+
disabled={!prompt || status === "processing"}
|
| 1055 |
+
>
|
| 1056 |
+
{status === "processing" ? "Generating..." : "Generate Video"}
|
| 1057 |
+
</button>
|
| 1058 |
+
|
| 1059 |
+
{status === "processing" && (
|
| 1060 |
+
<div className="status">
|
| 1061 |
+
<div className="progress-bar">
|
| 1062 |
+
<div style={{ width: `${progress}%` }} />
|
| 1063 |
+
</div>
|
| 1064 |
+
<p>Progress: {progress}%</p>
|
| 1065 |
+
</div>
|
| 1066 |
+
)}
|
| 1067 |
+
|
| 1068 |
+
{status === "completed" && jobId && (
|
| 1069 |
+
<VideoPlayer jobId={jobId} />
|
| 1070 |
+
)}
|
| 1071 |
+
</div>
|
| 1072 |
+
);
|
| 1073 |
+
}
|
| 1074 |
+
```
|
| 1075 |
+
|
| 1076 |
+
## Troubleshooting
|
| 1077 |
+
|
| 1078 |
+
### Common Issues
|
| 1079 |
+
|
| 1080 |
+
1. **Token Expiry**: Implement automatic token refresh
|
| 1081 |
+
2. **CORS Issues**: Configure Clerk allowed origins properly
|
| 1082 |
+
3. **Webhook Failures**: Verify webhook URL accessibility
|
| 1083 |
+
4. **Rate Limiting**: Implement proper rate limiting and user feedback
|
| 1084 |
+
|
| 1085 |
+
### Debug Tools
|
| 1086 |
+
|
| 1087 |
+
```typescript
|
| 1088 |
+
// Debug helper for authentication issues
|
| 1089 |
+
export function useAuthDebug() {
|
| 1090 |
+
const { isLoaded, isSignedIn, user, getToken } = useAuth();
|
| 1091 |
+
|
| 1092 |
+
const debugAuth = async () => {
|
| 1093 |
+
console.log('Auth Debug Info:', {
|
| 1094 |
+
isLoaded,
|
| 1095 |
+
isSignedIn,
|
| 1096 |
+
userId: user?.id,
|
| 1097 |
+
email: user?.primaryEmailAddress?.emailAddress,
|
| 1098 |
+
metadata: user?.publicMetadata
|
| 1099 |
+
});
|
| 1100 |
+
|
| 1101 |
+
if (isSignedIn) {
|
| 1102 |
+
try {
|
| 1103 |
+
const token = await getToken({ template: "t2m-api" });
|
| 1104 |
+
console.log('Token:', token);
|
| 1105 |
+
|
| 1106 |
+
if (token) {
|
| 1107 |
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
| 1108 |
+
console.log('Token payload:', payload);
|
| 1109 |
+
}
|
| 1110 |
+
} catch (error) {
|
| 1111 |
+
console.error('Token error:', error);
|
| 1112 |
+
}
|
| 1113 |
+
}
|
| 1114 |
+
};
|
| 1115 |
+
|
| 1116 |
+
return { debugAuth };
|
| 1117 |
+
}
|
| 1118 |
+
```
|
| 1119 |
+
|
| 1120 |
+
This comprehensive guide covers all aspects of authentication and session management for the T2M application, ensuring secure and efficient user experience while maintaining best practices for token handling and file access.
|
CLERK_TOKEN_GUIDE.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Getting Your Clerk Token - Step by Step Guide
|
| 2 |
+
|
| 3 |
+
## 🔍 Your Clerk Configuration
|
| 4 |
+
|
| 5 |
+
Based on the API response, your Clerk app (`poetic-primate-48.clerk.accounts.dev`) is configured to only support:
|
| 6 |
+
- ✅ Google OAuth (`oauth_google`)
|
| 7 |
+
- ✅ Ticket-based authentication (`ticket`)
|
| 8 |
+
- ❌ Email/Password authentication (not enabled)
|
| 9 |
+
|
| 10 |
+
## 🎯 Recommended Method: Browser Console
|
| 11 |
+
|
| 12 |
+
This is the easiest and most reliable method:
|
| 13 |
+
|
| 14 |
+
### Step 1: Sign in via Google OAuth
|
| 15 |
+
1. Open your browser
|
| 16 |
+
2. Go to: `https://poetic-primate-48.clerk.accounts.dev`
|
| 17 |
+
3. Click "Sign in with Google"
|
| 18 |
+
4. Complete the Google OAuth flow
|
| 19 |
+
|
| 20 |
+
### Step 2: Get the Token
|
| 21 |
+
1. Once signed in, open Developer Tools (Press F12)
|
| 22 |
+
2. Go to the **Console** tab
|
| 23 |
+
3. Paste this command and press Enter:
|
| 24 |
+
```javascript
|
| 25 |
+
window.Clerk.session.getToken().then(token => console.log('TOKEN:', token))
|
| 26 |
+
```
|
| 27 |
+
4. Copy the token (the long string after "TOKEN:")
|
| 28 |
+
|
| 29 |
+
### Step 3: Test Your API
|
| 30 |
+
```bash
|
| 31 |
+
python test_current_api.py http://localhost:8000/api/v1 YOUR_TOKEN_HERE
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## 🚀 Quick Start Scripts
|
| 35 |
+
|
| 36 |
+
### Option A: Use the OAuth Helper
|
| 37 |
+
```bash
|
| 38 |
+
python get_clerk_token_oauth.py
|
| 39 |
+
```
|
| 40 |
+
Choose option 2 (Manual browser method)
|
| 41 |
+
|
| 42 |
+
### Option B: Use the Simple Helper
|
| 43 |
+
```bash
|
| 44 |
+
python get_token_simple.py
|
| 45 |
+
```
|
| 46 |
+
Choose option 1 (Browser Console Method)
|
| 47 |
+
|
| 48 |
+
## 📋 Complete Example
|
| 49 |
+
|
| 50 |
+
1. **Open browser and sign in:**
|
| 51 |
+
- Go to: https://poetic-primate-48.clerk.accounts.dev
|
| 52 |
+
- Sign in with Google
|
| 53 |
+
|
| 54 |
+
2. **Get token in console:**
|
| 55 |
+
```javascript
|
| 56 |
+
window.Clerk.session.getToken().then(token => console.log('TOKEN:', token))
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
3. **Copy the token** (looks like: `eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18...`)
|
| 60 |
+
|
| 61 |
+
4. **Test your API:**
|
| 62 |
+
```bash
|
| 63 |
+
python test_current_api.py http://localhost:8000/api/v1 eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18...
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## 🔧 Why Email/Password Didn't Work
|
| 67 |
+
|
| 68 |
+
The API response showed:
|
| 69 |
+
```json
|
| 70 |
+
{
|
| 71 |
+
"status": "needs_identifier",
|
| 72 |
+
"supported_first_factors": [
|
| 73 |
+
{"strategy": "ticket"},
|
| 74 |
+
{"strategy": "oauth_google"}
|
| 75 |
+
]
|
| 76 |
+
}
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
This means your Clerk app is configured to only allow:
|
| 80 |
+
- Google OAuth sign-in
|
| 81 |
+
- Ticket-based authentication (for programmatic access)
|
| 82 |
+
|
| 83 |
+
Email/password authentication is not enabled in your Clerk dashboard.
|
| 84 |
+
|
| 85 |
+
## 🎯 Next Steps
|
| 86 |
+
|
| 87 |
+
Once you have your token:
|
| 88 |
+
|
| 89 |
+
1. **Test basic connectivity:**
|
| 90 |
+
```bash
|
| 91 |
+
python test_current_api.py http://localhost:8000/api/v1 YOUR_TOKEN
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
2. **Test video generation:**
|
| 95 |
+
```bash
|
| 96 |
+
python test_video_generation.py --base-url http://localhost:8000/api/v1 --token YOUR_TOKEN --quick
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
3. **Run comprehensive tests:**
|
| 100 |
+
```bash
|
| 101 |
+
python test_video_generation.py --base-url http://localhost:8000/api/v1 --token YOUR_TOKEN
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## 💡 Pro Tips
|
| 105 |
+
|
| 106 |
+
- **Tokens expire**: You may need to get a fresh token periodically
|
| 107 |
+
- **Save your token**: The scripts will save it to `auth_token.txt` for reuse
|
| 108 |
+
- **Use the browser method**: It's the most reliable approach
|
| 109 |
+
- **Check token format**: Valid tokens start with `eyJ`
|
| 110 |
+
|
| 111 |
+
## 🆘 Troubleshooting
|
| 112 |
+
|
| 113 |
+
**If the browser console method doesn't work:**
|
| 114 |
+
1. Make sure you're signed in to your app
|
| 115 |
+
2. Check that `window.Clerk` exists by typing it in console
|
| 116 |
+
3. Try refreshing the page after signing in
|
| 117 |
+
4. Make sure you're on the correct domain
|
| 118 |
+
|
| 119 |
+
**If you get "Clerk not found" errors:**
|
| 120 |
+
1. Make sure you're on a page where Clerk is loaded
|
| 121 |
+
2. Try going to the main app page after signing in
|
| 122 |
+
3. Check the Network tab for any Clerk-related requests
|
Dockerfile
CHANGED
|
@@ -1,105 +1,28 @@
|
|
| 1 |
-
|
| 2 |
-
FROM python:3.12-slim
|
| 3 |
|
| 4 |
# Set working directory
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
|
| 9 |
-
PYTHONUNBUFFERED=1 \
|
| 10 |
-
PYTHONPATH=/app:/app/src \
|
| 11 |
-
GRADIO_SERVER_NAME=0.0.0.0 \
|
| 12 |
-
GRADIO_SERVER_PORT=7860 \
|
| 13 |
-
HF_HOME=/app/.cache/huggingface \
|
| 14 |
-
HF_HUB_CACHE=/app/.cache/huggingface/hub \
|
| 15 |
-
TRANSFORMERS_CACHE=/app/.cache/transformers \
|
| 16 |
-
SENTENCE_TRANSFORMERS_HOME=/app/.cache/sentence_transformers \
|
| 17 |
-
PATH="/root/.TinyTeX/bin/x86_64-linux:$PATH" \
|
| 18 |
-
GRADIO_ALLOW_FLAGGING=never \
|
| 19 |
-
GRADIO_ANALYTICS_ENABLED=False \
|
| 20 |
-
HF_HUB_DISABLE_TELEMETRY=1
|
| 21 |
-
|
| 22 |
-
# Install system dependencies in single layer
|
| 23 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 24 |
-
wget \
|
| 25 |
-
curl \
|
| 26 |
-
git \
|
| 27 |
-
gcc \
|
| 28 |
-
g++ \
|
| 29 |
build-essential \
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
libasound2-dev \
|
| 33 |
-
libsdl-pango-dev \
|
| 34 |
-
libcairo2-dev \
|
| 35 |
-
libpango1.0-dev \
|
| 36 |
-
sox \
|
| 37 |
-
ffmpeg \
|
| 38 |
-
texlive-full \
|
| 39 |
-
dvisvgm \
|
| 40 |
-
ghostscript \
|
| 41 |
-
ca-certificates \
|
| 42 |
-
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
| 43 |
-
|
| 44 |
|
| 45 |
-
# Copy requirements
|
| 46 |
COPY requirements.txt .
|
| 47 |
-
RUN pip install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu \
|
| 48 |
-
&& pip install --no-cache-dir -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu \
|
| 49 |
-
&& python -c "import gradio; print(f'Gradio version: {gradio.__version__}')" \
|
| 50 |
-
&& find /usr/local -name "*.pyc" -delete \
|
| 51 |
-
&& find /usr/local -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
| 52 |
-
|
| 53 |
-
# Ensure Hugging Face cache directories are writable
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
RUN
|
| 57 |
-
&& echo "Downloading models for HF Spaces..." \
|
| 58 |
-
&& wget --progress=dot:giga --timeout=30 --tries=3 \
|
| 59 |
-
-O kokoro-v0_19.onnx \
|
| 60 |
-
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files/kokoro-v0_19.onnx" \
|
| 61 |
-
&& wget --progress=dot:giga --timeout=30 --tries=3 \
|
| 62 |
-
-O voices.bin \
|
| 63 |
-
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files/voices.bin" \
|
| 64 |
-
&& ls -la /app/models/
|
| 65 |
|
| 66 |
-
# Copy
|
| 67 |
COPY . .
|
| 68 |
|
| 69 |
-
|
| 70 |
-
RUN
|
| 71 |
-
|
| 72 |
-
# Run embedding creation script at build time
|
| 73 |
-
RUN python create_embeddings.py
|
| 74 |
-
|
| 75 |
-
# Ensure all files are writable (fix PermissionError for log file)
|
| 76 |
-
RUN chmod -R a+w /app
|
| 77 |
-
|
| 78 |
-
# Create output directory
|
| 79 |
-
RUN mkdir -p output tmp
|
| 80 |
-
# Ensure output and tmp directories are writable (fix PermissionError for session_id.txt)
|
| 81 |
-
RUN chmod -R a+w /app/output /app/tmp || true
|
| 82 |
-
|
| 83 |
-
RUN mkdir -p output tmp logs \
|
| 84 |
-
&& mkdir -p /app/.cache/huggingface/hub \
|
| 85 |
-
&& mkdir -p /app/.cache/transformers \
|
| 86 |
-
&& mkdir -p /app/.cache/sentence_transformers \
|
| 87 |
-
&& chmod -R 755 /app/.cache \
|
| 88 |
-
&& chmod 755 /app/models \
|
| 89 |
-
&& ls -la /app/models/ \
|
| 90 |
-
&& echo "Cache directories created with proper permissions"
|
| 91 |
-
# Add HF Spaces specific metadata
|
| 92 |
-
LABEL space.title="Text 2 Mnaim" \
|
| 93 |
-
space.sdk="docker" \
|
| 94 |
-
space.author="khanhthanhdev" \
|
| 95 |
-
space.description="Text to science video using multi Agent"
|
| 96 |
-
|
| 97 |
-
# Expose the port that HF Spaces expects
|
| 98 |
-
EXPOSE 7860
|
| 99 |
|
| 100 |
-
#
|
| 101 |
-
|
| 102 |
-
CMD curl -f http://localhost:7860/ || exit 1
|
| 103 |
|
| 104 |
-
#
|
| 105 |
-
CMD ["python", "
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
|
|
|
| 2 |
|
| 3 |
# Set working directory
|
| 4 |
WORKDIR /app
|
| 5 |
|
| 6 |
+
# Install system dependencies
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
build-essential \
|
| 9 |
+
curl \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
# Copy requirements first for better caching
|
| 13 |
COPY requirements.txt .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
# Install Python dependencies
|
| 16 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
# Copy application code
|
| 19 |
COPY . .
|
| 20 |
|
| 21 |
+
# Create necessary directories
|
| 22 |
+
RUN mkdir -p uploads videos logs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
# Expose port
|
| 25 |
+
EXPOSE 8000
|
|
|
|
| 26 |
|
| 27 |
+
# Command to run the application
|
| 28 |
+
CMD ["python", "-m", "uvicorn", "src.app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
FASTAPI_SETUP_GUIDE.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FastAPI Server Setup Guide
|
| 2 |
+
|
| 3 |
+
This guide will help you set up and run the FastAPI server for the T2M (Text-to-Media) video generation system.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- Python 3.11 or higher
|
| 8 |
+
- Redis server (for caching and job queuing)
|
| 9 |
+
- Git (for cloning the repository)
|
| 10 |
+
|
| 11 |
+
## Quick Start
|
| 12 |
+
|
| 13 |
+
### 1. Environment Setup
|
| 14 |
+
|
| 15 |
+
Your `.env` file is already configured with your Clerk keys. You just need to add the webhook secret once you create the webhook endpoint.
|
| 16 |
+
|
| 17 |
+
### 2. Get ngrok Auth Token (if not already set)
|
| 18 |
+
|
| 19 |
+
If you don't have an ngrok auth token in your `.env` file:
|
| 20 |
+
|
| 21 |
+
1. Go to [ngrok Dashboard](https://dashboard.ngrok.com/get-started/your-authtoken)
|
| 22 |
+
2. Sign up/login and copy your auth token
|
| 23 |
+
3. Add it to your `.env` file:
|
| 24 |
+
```env
|
| 25 |
+
NGROK_AUTHTOKEN=your_token_here
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### 3. Quick Setup with Docker (Recommended)
|
| 29 |
+
|
| 30 |
+
Run the automated setup script:
|
| 31 |
+
|
| 32 |
+
**On Windows:**
|
| 33 |
+
|
| 34 |
+
```cmd
|
| 35 |
+
setup-dev.bat
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
**On Linux/macOS:**
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
chmod +x setup-dev.sh
|
| 42 |
+
./setup-dev.sh
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
**Or manually with Make:**
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
make dev-services
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### 4. Get Your Public ngrok URL
|
| 52 |
+
|
| 53 |
+
After running the setup script:
|
| 54 |
+
|
| 55 |
+
1. Visit the ngrok dashboard: http://localhost:4040
|
| 56 |
+
2. Copy the HTTPS URL (e.g., `https://abc123.ngrok.io`)
|
| 57 |
+
3. This is your public URL for webhooks
|
| 58 |
+
|
| 59 |
+
**Or get it via command:**
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
make get-ngrok-url
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### 5. Configure Clerk Webhook
|
| 66 |
+
|
| 67 |
+
1. Go to [Clerk Dashboard](https://dashboard.clerk.com/)
|
| 68 |
+
2. Navigate to **Webhooks**
|
| 69 |
+
3. Click **"Add Endpoint"**
|
| 70 |
+
4. Enter your ngrok URL + webhook path:
|
| 71 |
+
```
|
| 72 |
+
https://your-ngrok-url.ngrok.io/api/v1/auth/webhooks/clerk
|
| 73 |
+
```
|
| 74 |
+
5. Select events: `user.created`, `user.updated`, `user.deleted`, `session.created`, `session.ended`
|
| 75 |
+
6. Copy the **Signing Secret** and add it to your `.env`:
|
| 76 |
+
```env
|
| 77 |
+
CLERK_WEBHOOK_SECRET=whsec_your_webhook_signing_secret_here
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### 6. Install Python Dependencies
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
pip install -r requirements.txt
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### 7. Start the FastAPI Server
|
| 87 |
+
|
| 88 |
+
#### Method 1: Using the Makefile (Recommended)
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
make serve-api
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
#### Method 2: Using uvicorn directly
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
python -m uvicorn src.app.main:app --reload --host 0.0.0.0 --port 8000
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
#### Method 3: Full development workflow (services + API)
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
make dev-start
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
## Verification
|
| 107 |
+
|
| 108 |
+
Once the server is running, you can verify it's working by:
|
| 109 |
+
|
| 110 |
+
### 1. Health Check
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
curl http://localhost:8000/health
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Expected response:
|
| 117 |
+
|
| 118 |
+
```json
|
| 119 |
+
{
|
| 120 |
+
"status": "healthy",
|
| 121 |
+
"app_name": "FastAPI Video Backend",
|
| 122 |
+
"version": "0.1.0",
|
| 123 |
+
"environment": "development"
|
| 124 |
+
}
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
### 2. API Documentation
|
| 128 |
+
|
| 129 |
+
Visit these URLs in your browser:
|
| 130 |
+
|
| 131 |
+
- **Local Swagger UI**: http://localhost:8000/docs
|
| 132 |
+
- **Public Swagger UI**: https://your-ngrok-url.ngrok.io/docs
|
| 133 |
+
- **ReDoc**: http://localhost:8000/redoc
|
| 134 |
+
- **OpenAPI JSON**: http://localhost:8000/openapi.json
|
| 135 |
+
- **ngrok Dashboard**: http://localhost:4040
|
| 136 |
+
|
| 137 |
+
### 3. Test Public Endpoint
|
| 138 |
+
|
| 139 |
+
```bash
|
| 140 |
+
# Test local endpoint
|
| 141 |
+
curl http://localhost:8000/
|
| 142 |
+
|
| 143 |
+
# Test public ngrok endpoint
|
| 144 |
+
curl https://your-ngrok-url.ngrok.io/health
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
## Docker Services Management
|
| 148 |
+
|
| 149 |
+
### Available Commands
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
# Start development services (Redis + ngrok)
|
| 153 |
+
make dev-services
|
| 154 |
+
|
| 155 |
+
# Stop development services
|
| 156 |
+
make dev-services-stop
|
| 157 |
+
|
| 158 |
+
# View service logs
|
| 159 |
+
make dev-services-logs
|
| 160 |
+
|
| 161 |
+
# Get current ngrok public URL
|
| 162 |
+
make get-ngrok-url
|
| 163 |
+
|
| 164 |
+
# Full development workflow
|
| 165 |
+
make dev-start
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
### Manual Docker Commands
|
| 169 |
+
|
| 170 |
+
```bash
|
| 171 |
+
# Start services
|
| 172 |
+
docker-compose -f docker-compose.dev.yml up -d
|
| 173 |
+
|
| 174 |
+
# Stop services
|
| 175 |
+
docker-compose -f docker-compose.dev.yml down
|
| 176 |
+
|
| 177 |
+
# View logs
|
| 178 |
+
docker-compose -f docker-compose.dev.yml logs -f
|
| 179 |
+
|
| 180 |
+
# Check service status
|
| 181 |
+
docker-compose -f docker-compose.dev.yml ps
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
## Available API Endpoints
|
| 185 |
+
|
| 186 |
+
The server provides the following main endpoint groups:
|
| 187 |
+
|
| 188 |
+
- **Authentication** (`/api/v1/auth/*`) - User authentication and authorization
|
| 189 |
+
- **Videos** (`/api/v1/videos/*`) - Video generation and management
|
| 190 |
+
- **Jobs** (`/api/v1/jobs/*`) - Background job management
|
| 191 |
+
- **Files** (`/api/v1/files/*`) - File upload and management
|
| 192 |
+
- **System** (`/api/v1/system/*`) - System health and monitoring
|
| 193 |
+
|
| 194 |
+
## Development Features
|
| 195 |
+
|
| 196 |
+
### Auto-reload
|
| 197 |
+
|
| 198 |
+
The server runs with auto-reload enabled in development mode, so changes to your code will automatically restart the server.
|
| 199 |
+
|
| 200 |
+
### Debug Mode
|
| 201 |
+
|
| 202 |
+
When `DEBUG=true` in your `.env` file:
|
| 203 |
+
|
| 204 |
+
- Detailed error messages are shown
|
| 205 |
+
- API documentation is available
|
| 206 |
+
- CORS is configured for local development
|
| 207 |
+
|
| 208 |
+
### Logging
|
| 209 |
+
|
| 210 |
+
The application uses structured logging. Logs will show:
|
| 211 |
+
|
| 212 |
+
- Request/response information
|
| 213 |
+
- Performance metrics
|
| 214 |
+
- Error details
|
| 215 |
+
- Authentication events
|
| 216 |
+
|
| 217 |
+
## Troubleshooting
|
| 218 |
+
|
| 219 |
+
### Common Issues
|
| 220 |
+
|
| 221 |
+
#### 1. Redis Connection Error
|
| 222 |
+
|
| 223 |
+
```
|
| 224 |
+
Failed to initialize Redis: [Errno 111] Connection refused
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
**Solution**: Make sure Redis server is running on the configured host and port.
|
| 228 |
+
|
| 229 |
+
#### 2. Clerk Authentication Error
|
| 230 |
+
|
| 231 |
+
```
|
| 232 |
+
Failed to initialize Clerk: Invalid secret key
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
**Solution**: Verify your Clerk API keys in the `.env` file are correct.
|
| 236 |
+
|
| 237 |
+
#### 3. Port Already in Use
|
| 238 |
+
|
| 239 |
+
```
|
| 240 |
+
[Errno 48] Address already in use
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
**Solution**: Either stop the process using port 8000 or change the `PORT` in your `.env` file.
|
| 244 |
+
|
| 245 |
+
#### 4. Import Errors
|
| 246 |
+
|
| 247 |
+
```
|
| 248 |
+
ModuleNotFoundError: No module named 'src'
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
**Solution**: Make sure you're running the server from the project root directory.
|
| 252 |
+
|
| 253 |
+
### Checking Server Status
|
| 254 |
+
|
| 255 |
+
```bash
|
| 256 |
+
# Check if server is running
|
| 257 |
+
curl -f http://localhost:8000/health
|
| 258 |
+
|
| 259 |
+
# Check Redis connection
|
| 260 |
+
redis-cli ping
|
| 261 |
+
|
| 262 |
+
# View server logs (if running in background)
|
| 263 |
+
tail -f logs/app.log
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
## Production Deployment
|
| 267 |
+
|
| 268 |
+
For production deployment:
|
| 269 |
+
|
| 270 |
+
1. Set `ENVIRONMENT=production` in your `.env`
|
| 271 |
+
2. Set `DEBUG=false`
|
| 272 |
+
3. Use a strong `SECRET_KEY`
|
| 273 |
+
4. Configure proper `ALLOWED_ORIGINS` for CORS
|
| 274 |
+
5. Set up proper Redis configuration with authentication
|
| 275 |
+
6. Use a production ASGI server like Gunicorn with Uvicorn workers
|
| 276 |
+
|
| 277 |
+
```bash
|
| 278 |
+
# Production server example
|
| 279 |
+
gunicorn src.app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
## Next Steps
|
| 283 |
+
|
| 284 |
+
After setting up the server:
|
| 285 |
+
|
| 286 |
+
1. **Generate Client SDKs**: Use `make generate-clients` to create client libraries
|
| 287 |
+
2. **Test the API**: Use `make test-clients` to run integration tests
|
| 288 |
+
3. **Explore the Documentation**: Visit `/docs` to understand available endpoints
|
| 289 |
+
4. **Set up Authentication**: Configure Clerk for user management
|
| 290 |
+
5. **Upload Files**: Test file upload functionality through the API
|
| 291 |
+
|
| 292 |
+
## Support
|
| 293 |
+
|
| 294 |
+
If you encounter issues:
|
| 295 |
+
|
| 296 |
+
1. Check the server logs for detailed error messages
|
| 297 |
+
2. Verify all environment variables are set correctly
|
| 298 |
+
3. Ensure all dependencies are installed
|
| 299 |
+
4. Make sure Redis is running and accessible
|
| 300 |
+
5. Check that your API keys are valid and have proper permissions
|
| 301 |
+
|
| 302 |
+
For additional help, refer to the project documentation or create an issue in the repository.
|
Makefile
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Video Generation API - Client Generation Makefile
|
| 2 |
+
# This Makefile provides convenient commands for generating and testing client SDKs
|
| 3 |
+
|
| 4 |
+
.PHONY: help install-deps generate-clients test-clients clean-clients docs serve-api
|
| 5 |
+
|
| 6 |
+
# Default target
|
| 7 |
+
help:
|
| 8 |
+
@echo "Video Generation API - Client Generation"
|
| 9 |
+
@echo ""
|
| 10 |
+
@echo "Available targets:"
|
| 11 |
+
@echo " help Show this help message"
|
| 12 |
+
@echo " install-deps Install required dependencies"
|
| 13 |
+
@echo " generate-clients Generate all client SDKs"
|
| 14 |
+
@echo " generate-ts Generate TypeScript client only"
|
| 15 |
+
@echo " generate-python Generate Python client only"
|
| 16 |
+
@echo " generate-java Generate Java client only"
|
| 17 |
+
@echo " test-clients Test generated clients"
|
| 18 |
+
@echo " clean-clients Clean generated clients"
|
| 19 |
+
@echo " serve-api Start API server for testing"
|
| 20 |
+
@echo " docs Generate documentation"
|
| 21 |
+
@echo ""
|
| 22 |
+
@echo "Environment variables:"
|
| 23 |
+
@echo " API_URL API base URL (default: http://localhost:8000)"
|
| 24 |
+
@echo " OUTPUT_DIR Output directory (default: generated_clients)"
|
| 25 |
+
@echo " LANGUAGES Space-separated list of languages"
|
| 26 |
+
|
| 27 |
+
# Configuration
|
| 28 |
+
API_URL ?= http://localhost:8000
|
| 29 |
+
OUTPUT_DIR ?= generated_clients
|
| 30 |
+
LANGUAGES ?= typescript python java csharp go php ruby
|
| 31 |
+
|
| 32 |
+
# Install dependencies
|
| 33 |
+
install-deps:
|
| 34 |
+
@echo "Installing dependencies..."
|
| 35 |
+
@command -v node >/dev/null 2>&1 || { echo "Node.js is required but not installed. Please install Node.js first."; exit 1; }
|
| 36 |
+
@command -v npm >/dev/null 2>&1 || { echo "npm is required but not installed. Please install npm first."; exit 1; }
|
| 37 |
+
@echo "Installing OpenAPI Generator CLI..."
|
| 38 |
+
npm install -g @openapitools/openapi-generator-cli
|
| 39 |
+
@echo "Dependencies installed successfully!"
|
| 40 |
+
|
| 41 |
+
# Generate all clients
|
| 42 |
+
generate-clients: check-api
|
| 43 |
+
@echo "Generating client SDKs for languages: $(LANGUAGES)"
|
| 44 |
+
python scripts/generate_clients.py \
|
| 45 |
+
--api-url $(API_URL) \
|
| 46 |
+
--output-dir $(OUTPUT_DIR) \
|
| 47 |
+
--languages $(LANGUAGES)
|
| 48 |
+
|
| 49 |
+
# Generate TypeScript client
|
| 50 |
+
generate-ts: check-api
|
| 51 |
+
@echo "Generating TypeScript client..."
|
| 52 |
+
python scripts/generate_clients.py \
|
| 53 |
+
--api-url $(API_URL) \
|
| 54 |
+
--output-dir $(OUTPUT_DIR) \
|
| 55 |
+
--languages typescript
|
| 56 |
+
|
| 57 |
+
# Generate Python client
|
| 58 |
+
generate-python: check-api
|
| 59 |
+
@echo "Generating Python client..."
|
| 60 |
+
python scripts/generate_clients.py \
|
| 61 |
+
--api-url $(API_URL) \
|
| 62 |
+
--output-dir $(OUTPUT_DIR) \
|
| 63 |
+
--languages python
|
| 64 |
+
|
| 65 |
+
# Generate Java client
|
| 66 |
+
generate-java: check-api
|
| 67 |
+
@echo "Generating Java client..."
|
| 68 |
+
python scripts/generate_clients.py \
|
| 69 |
+
--api-url $(API_URL) \
|
| 70 |
+
--output-dir $(OUTPUT_DIR) \
|
| 71 |
+
--languages java
|
| 72 |
+
|
| 73 |
+
# Test generated clients
|
| 74 |
+
test-clients:
|
| 75 |
+
@echo "Testing generated clients..."
|
| 76 |
+
python scripts/test_clients.py \
|
| 77 |
+
--api-url $(API_URL) \
|
| 78 |
+
--clients-dir $(OUTPUT_DIR) \
|
| 79 |
+
--verbose
|
| 80 |
+
|
| 81 |
+
# Clean generated clients
|
| 82 |
+
clean-clients:
|
| 83 |
+
@echo "Cleaning generated clients..."
|
| 84 |
+
rm -rf $(OUTPUT_DIR)
|
| 85 |
+
@echo "Generated clients cleaned!"
|
| 86 |
+
|
| 87 |
+
# Check if API is running
|
| 88 |
+
check-api:
|
| 89 |
+
@echo "Checking if API is running at $(API_URL)..."
|
| 90 |
+
@curl -s -f $(API_URL)/health > /dev/null || { \
|
| 91 |
+
echo "API is not running at $(API_URL)"; \
|
| 92 |
+
echo "Please start the API server first with: make serve-api"; \
|
| 93 |
+
exit 1; \
|
| 94 |
+
}
|
| 95 |
+
@echo "API is running!"
|
| 96 |
+
|
| 97 |
+
# Start API server
|
| 98 |
+
serve-api:
|
| 99 |
+
@echo "Starting API server..."
|
| 100 |
+
python -m uvicorn src.app.main:app --reload --host 0.0.0.0 --port 8000
|
| 101 |
+
|
| 102 |
+
# Development environment setup
|
| 103 |
+
dev-setup-full: install-deps
|
| 104 |
+
@echo "Setting up full development environment with Docker..."
|
| 105 |
+
@chmod +x setup-dev.sh
|
| 106 |
+
@./setup-dev.sh
|
| 107 |
+
|
| 108 |
+
# Start development services (Redis + ngrok)
|
| 109 |
+
dev-services:
|
| 110 |
+
@echo "Starting development services (Redis + ngrok)..."
|
| 111 |
+
docker-compose -f docker-compose.dev.yml up -d
|
| 112 |
+
@echo "Services started! Visit http://localhost:4040 for ngrok dashboard"
|
| 113 |
+
|
| 114 |
+
# Stop development services
|
| 115 |
+
dev-services-stop:
|
| 116 |
+
@echo "Stopping development services..."
|
| 117 |
+
docker-compose -f docker-compose.dev.yml down
|
| 118 |
+
|
| 119 |
+
# View development services logs
|
| 120 |
+
dev-services-logs:
|
| 121 |
+
docker-compose -f docker-compose.dev.yml logs -f
|
| 122 |
+
|
| 123 |
+
# Full development workflow
|
| 124 |
+
dev-start: dev-services serve-api
|
| 125 |
+
|
| 126 |
+
# Get ngrok public URL
|
| 127 |
+
get-ngrok-url:
|
| 128 |
+
@echo "Getting ngrok public URL..."
|
| 129 |
+
@curl -s http://localhost:4040/api/tunnels | python -c "import sys, json; data = json.load(sys.stdin); print(data['tunnels'][0]['public_url'] if data['tunnels'] else 'No tunnels found')" 2>/dev/null || echo "ngrok not running or no tunnels found"
|
| 130 |
+
|
| 131 |
+
# Generate documentation
|
| 132 |
+
docs:
|
| 133 |
+
@echo "Generating documentation..."
|
| 134 |
+
@mkdir -p docs/generated
|
| 135 |
+
@echo "Documentation generated in docs/ directory"
|
| 136 |
+
|
| 137 |
+
# Development targets
|
| 138 |
+
dev-setup: install-deps
|
| 139 |
+
@echo "Setting up development environment..."
|
| 140 |
+
@chmod +x scripts/generate_clients.sh
|
| 141 |
+
@echo "Development environment ready!"
|
| 142 |
+
|
| 143 |
+
# Quick test - generate and test TypeScript client
|
| 144 |
+
quick-test: generate-ts
|
| 145 |
+
@echo "Running quick test with TypeScript client..."
|
| 146 |
+
cd $(OUTPUT_DIR)/typescript && npm install --silent
|
| 147 |
+
@echo "Quick test completed!"
|
| 148 |
+
|
| 149 |
+
# Full workflow - generate all clients and test
|
| 150 |
+
full-workflow: generate-clients test-clients
|
| 151 |
+
@echo "Full client generation and testing workflow completed!"
|
| 152 |
+
|
| 153 |
+
# Docker targets
|
| 154 |
+
docker-generate:
|
| 155 |
+
@echo "Generating clients using Docker..."
|
| 156 |
+
docker run --rm \
|
| 157 |
+
-v $(PWD):/workspace \
|
| 158 |
+
-w /workspace \
|
| 159 |
+
node:18-alpine \
|
| 160 |
+
sh -c "npm install -g @openapitools/openapi-generator-cli && python scripts/generate_clients.py"
|
| 161 |
+
|
| 162 |
+
# CI/CD targets
|
| 163 |
+
ci-test: install-deps generate-clients test-clients
|
| 164 |
+
@echo "CI/CD pipeline completed successfully!"
|
| 165 |
+
|
| 166 |
+
# Validate OpenAPI spec
|
| 167 |
+
validate-spec: check-api
|
| 168 |
+
@echo "Validating OpenAPI specification..."
|
| 169 |
+
curl -s $(API_URL)/openapi.json | python -m json.tool > /dev/null
|
| 170 |
+
@echo "OpenAPI specification is valid!"
|
| 171 |
+
|
| 172 |
+
# Show client statistics
|
| 173 |
+
stats:
|
| 174 |
+
@echo "Client generation statistics:"
|
| 175 |
+
@if [ -d "$(OUTPUT_DIR)" ]; then \
|
| 176 |
+
echo "Generated clients:"; \
|
| 177 |
+
for dir in $(OUTPUT_DIR)/*/; do \
|
| 178 |
+
if [ -d "$$dir" ]; then \
|
| 179 |
+
lang=$$(basename "$$dir"); \
|
| 180 |
+
files=$$(find "$$dir" -type f | wc -l); \
|
| 181 |
+
size=$$(du -sh "$$dir" | cut -f1); \
|
| 182 |
+
echo " $$lang: $$files files, $$size"; \
|
| 183 |
+
fi; \
|
| 184 |
+
done; \
|
| 185 |
+
else \
|
| 186 |
+
echo "No clients generated yet. Run 'make generate-clients' first."; \
|
| 187 |
+
fi
|
| 188 |
+
|
| 189 |
+
# Package clients for distribution
|
| 190 |
+
package:
|
| 191 |
+
@echo "Packaging clients for distribution..."
|
| 192 |
+
@mkdir -p dist
|
| 193 |
+
@if [ -d "$(OUTPUT_DIR)" ]; then \
|
| 194 |
+
cd $(OUTPUT_DIR) && \
|
| 195 |
+
for dir in */; do \
|
| 196 |
+
if [ -d "$$dir" ]; then \
|
| 197 |
+
lang=$$(basename "$$dir"); \
|
| 198 |
+
echo "Packaging $$lang client..."; \
|
| 199 |
+
tar -czf ../dist/video-api-client-$$lang.tar.gz "$$dir"; \
|
| 200 |
+
fi; \
|
| 201 |
+
done; \
|
| 202 |
+
fi
|
| 203 |
+
@echo "Clients packaged in dist/ directory"
|
| 204 |
+
|
| 205 |
+
# Publish clients (placeholder - customize for your needs)
|
| 206 |
+
publish:
|
| 207 |
+
@echo "Publishing clients..."
|
| 208 |
+
@echo "This is a placeholder. Customize for your package managers."
|
| 209 |
+
@if [ -d "$(OUTPUT_DIR)/typescript" ]; then \
|
| 210 |
+
echo "Would publish TypeScript client to npm"; \
|
| 211 |
+
fi
|
| 212 |
+
@if [ -d "$(OUTPUT_DIR)/python" ]; then \
|
| 213 |
+
echo "Would publish Python client to PyPI"; \
|
| 214 |
+
fi
|
| 215 |
+
@if [ -d "$(OUTPUT_DIR)/java" ]; then \
|
| 216 |
+
echo "Would publish Java client to Maven Central"; \
|
| 217 |
+
fi
|
| 218 |
+
|
| 219 |
+
# Watch for changes and regenerate
|
| 220 |
+
watch:
|
| 221 |
+
@echo "Watching for API changes and regenerating clients..."
|
| 222 |
+
@echo "This requires 'entr' to be installed: brew install entr"
|
| 223 |
+
@echo "Watching src/app/ for changes..."
|
| 224 |
+
find src/app -name "*.py" | entr -r make generate-clients
|
| 225 |
+
|
| 226 |
+
# Benchmark client generation
|
| 227 |
+
benchmark:
|
| 228 |
+
@echo "Benchmarking client generation..."
|
| 229 |
+
@time make generate-clients
|
| 230 |
+
@echo "Benchmark completed!"
|
| 231 |
+
|
| 232 |
+
# Show OpenAPI spec
|
| 233 |
+
show-spec: check-api
|
| 234 |
+
@echo "OpenAPI Specification:"
|
| 235 |
+
@curl -s $(API_URL)/openapi.json | python -m json.tool
|
| 236 |
+
|
| 237 |
+
# Interactive mode
|
| 238 |
+
interactive:
|
| 239 |
+
@echo "Interactive client generation mode"
|
| 240 |
+
@echo "Available languages: typescript python java csharp go php ruby"
|
| 241 |
+
@read -p "Enter languages to generate (space-separated): " langs; \
|
| 242 |
+
make generate-clients LANGUAGES="$$langs"
|
| 243 |
+
|
| 244 |
+
# Health check
|
| 245 |
+
health:
|
| 246 |
+
@echo "Performing health check..."
|
| 247 |
+
@make check-api
|
| 248 |
+
@command -v openapi-generator-cli >/dev/null 2>&1 || { echo "OpenAPI Generator CLI not found"; exit 1; }
|
| 249 |
+
@command -v python >/dev/null 2>&1 || { echo "Python not found"; exit 1; }
|
| 250 |
+
@echo "All dependencies are available!"
|
| 251 |
+
|
| 252 |
+
# Show version information
|
| 253 |
+
version:
|
| 254 |
+
@echo "Version information:"
|
| 255 |
+
@echo "Node.js: $$(node --version 2>/dev/null || echo 'Not installed')"
|
| 256 |
+
@echo "npm: $$(npm --version 2>/dev/null || echo 'Not installed')"
|
| 257 |
+
@echo "Python: $$(python --version 2>/dev/null || echo 'Not installed')"
|
| 258 |
+
@echo "OpenAPI Generator: $$(openapi-generator-cli version 2>/dev/null || echo 'Not installed')"
|
| 259 |
+
|
| 260 |
+
# Example usage
|
| 261 |
+
example:
|
| 262 |
+
@echo "Example usage:"
|
| 263 |
+
@echo ""
|
| 264 |
+
@echo "1. Start the API server:"
|
| 265 |
+
@echo " make serve-api"
|
| 266 |
+
@echo ""
|
| 267 |
+
@echo "2. In another terminal, generate clients:"
|
| 268 |
+
@echo " make generate-clients"
|
| 269 |
+
@echo ""
|
| 270 |
+
@echo "3. Test the generated clients:"
|
| 271 |
+
@echo " make test-clients"
|
| 272 |
+
@echo ""
|
| 273 |
+
@echo "4. Generate specific language only:"
|
| 274 |
+
@echo " make generate-ts"
|
| 275 |
+
@echo " make generate-python"
|
| 276 |
+
@echo ""
|
| 277 |
+
@echo "5. Clean up:"
|
| 278 |
+
@echo " make clean-clients"
|
README.md
CHANGED
|
@@ -1,306 +1,205 @@
|
|
| 1 |
-
|
| 2 |
-
title: AI Animation & Voice Studio
|
| 3 |
-
emoji: 🎬
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: purple
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
suggested_hardware: cpu-upgrade
|
| 9 |
-
suggested_storage: large
|
| 10 |
-
pinned: true
|
| 11 |
-
license: apache-2.0
|
| 12 |
-
short_description: "Create mathematical animations with AI-powered using Manim"
|
| 13 |
-
tags:
|
| 14 |
-
- text-to-speech
|
| 15 |
-
- animation
|
| 16 |
-
- mathematics
|
| 17 |
-
- manim
|
| 18 |
-
- ai-voice
|
| 19 |
-
- educational
|
| 20 |
-
- visualization
|
| 21 |
-
models:
|
| 22 |
-
- kokoro-onnx/kokoro-v0_19
|
| 23 |
-
datasets: []
|
| 24 |
-
startup_duration_timeout: 30m
|
| 25 |
-
fullWidth: true
|
| 26 |
-
header: default
|
| 27 |
-
disable_embedding: false
|
| 28 |
-
preload_from_hub: []
|
| 29 |
-
---
|
| 30 |
-
|
| 31 |
-
# AI Animation & Voice Studio 🎬
|
| 32 |
-
|
| 33 |
-
A powerful application that combines AI-powered text-to-speech with mathematical animation generation using Manim and Kokoro TTS. Create stunning educational content with synchronized voice narration and mathematical visualizations.
|
| 34 |
-
|
| 35 |
-
## 🚀 Features
|
| 36 |
-
|
| 37 |
-
- **Text-to-Speech**: High-quality voice synthesis using Kokoro ONNX models
|
| 38 |
-
- **Mathematical Animations**: Create stunning mathematical visualizations with Manim
|
| 39 |
-
- **LaTeX Support**: Full LaTeX rendering capabilities with TinyTeX
|
| 40 |
-
- **Interactive Interface**: User-friendly Gradio web interface
|
| 41 |
-
- **Audio Processing**: Advanced audio manipulation with FFmpeg and SoX
|
| 42 |
-
|
| 43 |
-
## 🛠️ Technology Stack
|
| 44 |
-
|
| 45 |
-
- **Frontend**: Gradio for interactive web interface
|
| 46 |
-
- **Backend**: Python with FastAPI/Flask
|
| 47 |
-
- **Animation**: Manim (Mathematical Animation Engine)
|
| 48 |
-
- **TTS**: Kokoro ONNX for text-to-speech synthesis
|
| 49 |
-
- **LaTeX**: TinyTeX for mathematical typesetting
|
| 50 |
-
- **Audio**: FFmpeg, SoX, PortAudio for audio processing
|
| 51 |
-
- **Deployment**: Docker container optimized for Hugging Face Spaces
|
| 52 |
-
|
| 53 |
-
## 📦 Models
|
| 54 |
-
|
| 55 |
-
This application uses the following pre-trained models:
|
| 56 |
-
|
| 57 |
-
- **Kokoro TTS**: `kokoro-v0_19.onnx` - High-quality neural text-to-speech model
|
| 58 |
-
- **Voice Models**: `voices.bin` - Voice embedding models for different speaker characteristics
|
| 59 |
-
|
| 60 |
-
Models are automatically downloaded during the Docker build process from the official releases.
|
| 61 |
-
|
| 62 |
-
## 🏃♂️ Quick Start
|
| 63 |
-
|
| 64 |
-
### Using Hugging Face Spaces
|
| 65 |
-
|
| 66 |
-
1. Visit the [Space](https://huggingface.co/spaces/your-username/ai-animation-voice-studio)
|
| 67 |
-
2. Wait for the container to load (initial startup may take 3-5 minutes due to model loading)
|
| 68 |
-
3. Upload your script or enter text directly
|
| 69 |
-
4. Choose animation settings and voice parameters
|
| 70 |
-
5. Generate your animated video with AI narration!
|
| 71 |
-
|
| 72 |
-
### Local Development
|
| 73 |
|
| 74 |
-
|
| 75 |
-
# Clone the repository
|
| 76 |
-
git clone https://huggingface.co/spaces/your-username/ai-animation-voice-studio
|
| 77 |
-
cd ai-animation-voice-studio
|
| 78 |
-
|
| 79 |
-
# Build the Docker image
|
| 80 |
-
docker build -t ai-animation-studio .
|
| 81 |
-
|
| 82 |
-
# Run the container
|
| 83 |
-
docker run -p 7860:7860 ai-animation-studio
|
| 84 |
-
```
|
| 85 |
-
|
| 86 |
-
Access the application at `http://localhost:7860`
|
| 87 |
-
|
| 88 |
-
### Environment Setup
|
| 89 |
-
|
| 90 |
-
Create a `.env` file with your configuration:
|
| 91 |
-
|
| 92 |
-
```env
|
| 93 |
-
# Application settings
|
| 94 |
-
DEBUG=false
|
| 95 |
-
MAX_WORKERS=4
|
| 96 |
-
|
| 97 |
-
# Model settings
|
| 98 |
-
MODEL_PATH=/app/models
|
| 99 |
-
CACHE_DIR=/tmp/cache
|
| 100 |
-
|
| 101 |
-
# Optional: API keys if needed
|
| 102 |
-
# OPENAI_API_KEY=your_key_here
|
| 103 |
-
```
|
| 104 |
|
| 105 |
-
##
|
| 106 |
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
| 110 |
-
# Example usage in your code
|
| 111 |
-
from src.tts import generate_speech
|
| 112 |
|
| 113 |
-
|
| 114 |
-
text="Hello, this is a test of the text-to-speech system",
|
| 115 |
-
voice="default",
|
| 116 |
-
speed=1.0
|
| 117 |
-
)
|
| 118 |
-
```
|
| 119 |
-
|
| 120 |
-
### Mathematical Animation
|
| 121 |
-
|
| 122 |
-
```python
|
| 123 |
-
# Example Manim scene
|
| 124 |
-
from manim import *
|
| 125 |
-
|
| 126 |
-
class Example(Scene):
|
| 127 |
-
def construct(self):
|
| 128 |
-
# Your animation code here
|
| 129 |
-
pass
|
| 130 |
-
```
|
| 131 |
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
```
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
│ ├──
|
| 138 |
-
│
|
| 139 |
-
├──
|
| 140 |
-
|
| 141 |
-
├──
|
| 142 |
-
├──
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
```
|
| 146 |
|
| 147 |
-
##
|
| 148 |
|
| 149 |
-
|
| 150 |
|
| 151 |
-
|
| 152 |
-
- `GRADIO_SERVER_PORT`: Server port (default: 7860)
|
| 153 |
-
- `PYTHONPATH`: Python path configuration
|
| 154 |
-
- `HF_HOME`: Hugging Face cache directory
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
-
|
| 161 |
-
-
|
| 162 |
-
- Animation render settings
|
| 163 |
-
- Cache configurations
|
| 164 |
-
|
| 165 |
-
## 🔧 Development
|
| 166 |
-
|
| 167 |
-
### Prerequisites
|
| 168 |
|
| 169 |
-
|
| 170 |
-
- Python 3.12+
|
| 171 |
-
- Git
|
| 172 |
|
| 173 |
-
###
|
| 174 |
|
| 175 |
```bash
|
| 176 |
-
#
|
| 177 |
-
|
| 178 |
|
| 179 |
-
# Run
|
| 180 |
-
|
| 181 |
|
| 182 |
-
#
|
| 183 |
-
|
| 184 |
-
isort .
|
| 185 |
-
|
| 186 |
-
# Lint code
|
| 187 |
-
flake8 .
|
| 188 |
```
|
| 189 |
|
| 190 |
-
###
|
| 191 |
|
| 192 |
```bash
|
| 193 |
-
#
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
# Test the container locally
|
| 197 |
-
docker run --rm -p 7860:7860 your-app-name:dev
|
| 198 |
-
|
| 199 |
-
# Check container health
|
| 200 |
-
docker run --rm your-app-name:dev python -c "import src; print('Import successful')"
|
| 201 |
-
```
|
| 202 |
|
| 203 |
-
|
|
|
|
| 204 |
|
| 205 |
-
|
|
|
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
- **Memory Usage**: ~2-3GB during operation
|
| 211 |
|
| 212 |
-
###
|
| 213 |
|
| 214 |
-
-
|
| 215 |
-
-
|
| 216 |
-
-
|
| 217 |
-
- **Network**: Stable connection for initial model downloads
|
| 218 |
|
| 219 |
-
|
| 220 |
|
| 221 |
-
|
| 222 |
-
- Gradio interface uses efficient streaming for large outputs
|
| 223 |
-
- Docker multi-stage builds minimize final image size
|
| 224 |
-
- TinyTeX installation is optimized for essential packages only
|
| 225 |
|
| 226 |
-
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
|
| 229 |
|
| 230 |
-
|
| 231 |
-
```bash
|
| 232 |
-
# Clear Docker cache if build fails
|
| 233 |
-
docker system prune -a
|
| 234 |
-
docker build --no-cache -t your-app-name .
|
| 235 |
-
```
|
| 236 |
|
| 237 |
-
|
| 238 |
-
-
|
| 239 |
-
- Verify model URLs are accessible
|
| 240 |
-
- Models will be re-downloaded if corrupted
|
| 241 |
|
| 242 |
-
|
| 243 |
-
- Reduce batch sizes in configuration
|
| 244 |
-
- Monitor memory usage with `docker stats`
|
| 245 |
|
| 246 |
-
|
| 247 |
-
- Ensure audio drivers are properly installed
|
| 248 |
-
- Check PortAudio configuration
|
| 249 |
|
| 250 |
-
|
|
|
|
| 251 |
|
| 252 |
-
|
| 253 |
-
2. Review container logs in the Space settings
|
| 254 |
-
3. Enable debug mode in configuration
|
| 255 |
-
4. Report issues in the Community tab
|
| 256 |
|
| 257 |
-
|
| 258 |
|
| 259 |
-
**
|
| 260 |
-
-
|
| 261 |
-
-
|
| 262 |
-
-
|
|
|
|
| 263 |
|
| 264 |
-
|
| 265 |
-
- Models download automatically on first run
|
| 266 |
-
- Check Space logs for download progress
|
| 267 |
-
- Restart Space if models fail to load
|
| 268 |
-
|
| 269 |
-
## 🤝 Contributing
|
| 270 |
-
|
| 271 |
-
We welcome contributions! Please see our contributing guidelines:
|
| 272 |
|
| 273 |
1. Fork the repository
|
| 274 |
2. Create a feature branch
|
| 275 |
3. Make your changes
|
| 276 |
-
4. Add tests
|
| 277 |
-
5.
|
| 278 |
-
|
| 279 |
-
### Code Style
|
| 280 |
-
|
| 281 |
-
- Follow PEP 8 for Python code
|
| 282 |
-
- Use Black for code formatting
|
| 283 |
-
- Add docstrings for functions and classes
|
| 284 |
-
- Include type hints where appropriate
|
| 285 |
-
|
| 286 |
-
## 📄 License
|
| 287 |
-
|
| 288 |
-
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
| 289 |
-
|
| 290 |
-
## 🙏 Acknowledgments
|
| 291 |
-
|
| 292 |
-
- [Manim Community](https://www.manim.community/) for the animation engine
|
| 293 |
-
- [Kokoro TTS](https://github.com/thewh1teagle/kokoro-onnx) for text-to-speech models
|
| 294 |
-
- [Gradio](https://gradio.app/) for the web interface framework
|
| 295 |
-
- [Hugging Face](https://huggingface.co/) for hosting and infrastructure
|
| 296 |
-
|
| 297 |
-
## 📞 Contact
|
| 298 |
-
|
| 299 |
-
- **Author**: Your Name
|
| 300 |
-
- **Email**: [email protected]
|
| 301 |
-
- **GitHub**: [@your-username](https://github.com/your-username)
|
| 302 |
-
- **Hugging Face**: [@your-username](https://huggingface.co/your-username)
|
| 303 |
|
| 304 |
-
|
| 305 |
|
| 306 |
-
|
|
|
|
| 1 |
+
# FastAPI Video Backend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
A FastAPI backend for the multi-agent video generation system using Pydantic for data modeling, Clerk for authentication, and Redis for caching and job queuing.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
## Features
|
| 6 |
|
| 7 |
+
- **FastAPI Framework**: Modern, fast web framework for building APIs
|
| 8 |
+
- **Pydantic Models**: Data validation and serialization
|
| 9 |
+
- **Clerk Authentication**: Secure user authentication and management
|
| 10 |
+
- **Redis Integration**: Caching and job queue management
|
| 11 |
+
- **Structured Logging**: JSON-formatted logging with structlog
|
| 12 |
+
- **Environment Configuration**: Flexible configuration with Pydantic Settings
|
| 13 |
+
- **CORS Support**: Cross-origin resource sharing configuration
|
| 14 |
+
- **Health Checks**: Built-in health monitoring endpoints
|
| 15 |
+
- **Testing Suite**: Comprehensive test coverage with pytest
|
| 16 |
|
| 17 |
+
## Quick Start
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
### Prerequisites
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
- Python 3.11+
|
| 22 |
+
- Redis server
|
| 23 |
+
- Clerk account (for authentication)
|
| 24 |
+
|
| 25 |
+
### Installation
|
| 26 |
+
|
| 27 |
+
1. **Clone the repository**
|
| 28 |
+
```bash
|
| 29 |
+
git clone <repository-url>
|
| 30 |
+
cd fastapi-video-backend
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
2. **Install dependencies**
|
| 34 |
+
```bash
|
| 35 |
+
pip install -e .
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
3. **Set up development environment**
|
| 39 |
+
```bash
|
| 40 |
+
python scripts/setup.py
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
4. **Configure environment variables**
|
| 44 |
+
|
| 45 |
+
Update the `.env` file with your actual configuration:
|
| 46 |
+
```bash
|
| 47 |
+
# Required: Get these from your Clerk dashboard
|
| 48 |
+
CLERK_SECRET_KEY=your_actual_clerk_secret_key
|
| 49 |
+
CLERK_PUBLISHABLE_KEY=your_actual_clerk_publishable_key
|
| 50 |
+
|
| 51 |
+
# Required: Generate a secure secret key
|
| 52 |
+
SECRET_KEY=your_super_secret_key_here
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
5. **Start Redis server**
|
| 56 |
+
```bash
|
| 57 |
+
redis-server
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
6. **Run the application**
|
| 61 |
+
```bash
|
| 62 |
+
# Development mode
|
| 63 |
+
python -m src.app.main
|
| 64 |
+
|
| 65 |
+
# Or using the script
|
| 66 |
+
python -m uvicorn src.app.main:app --reload
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
7. **Access the API**
|
| 70 |
+
- API Documentation: http://localhost:8000/docs
|
| 71 |
+
- Alternative Docs: http://localhost:8000/redoc
|
| 72 |
+
- Health Check: http://localhost:8000/health
|
| 73 |
+
|
| 74 |
+
## Project Structure
|
| 75 |
|
| 76 |
```
|
| 77 |
+
src/
|
| 78 |
+
├── app/
|
| 79 |
+
│ ├── main.py # FastAPI application entry point
|
| 80 |
+
│ ├── api/ # API layer
|
| 81 |
+
│ │ ├── dependencies.py # Shared dependencies
|
| 82 |
+
│ │ └── v1/ # API version 1
|
| 83 |
+
│ │ ├── videos.py # Video generation endpoints
|
| 84 |
+
│ │ ├── jobs.py # Job management endpoints
|
| 85 |
+
│ │ └── system.py # System health endpoints
|
| 86 |
+
│ ├── core/ # Core utilities and configurations
|
| 87 |
+
│ │ ├── config.py # Application settings
|
| 88 |
+
│ │ ├── redis.py # Redis connection and utilities
|
| 89 |
+
│ │ ├── auth.py # Clerk authentication utilities
|
| 90 |
+
│ │ ├── logger.py # Logging configuration
|
| 91 |
+
│ │ └── exceptions.py # Custom exceptions
|
| 92 |
+
│ ├── services/ # Business logic layer
|
| 93 |
+
│ │ ├── video_service.py # Video generation business logic
|
| 94 |
+
│ │ ├── job_service.py # Job management logic
|
| 95 |
+
│ │ └── queue_service.py # Redis queue management
|
| 96 |
+
│ ├── models/ # Pydantic models
|
| 97 |
+
│ │ ├── job.py # Job data models
|
| 98 |
+
│ │ ├── video.py # Video metadata models
|
| 99 |
+
│ │ ├── user.py # User data models
|
| 100 |
+
│ │ └── system.py # System status models
|
| 101 |
+
│ ├── middleware/ # Custom middleware
|
| 102 |
+
│ │ ├── cors.py # CORS middleware
|
| 103 |
+
│ │ ├── clerk_auth.py # Clerk authentication middleware
|
| 104 |
+
│ │ └── error_handling.py # Global error handling
|
| 105 |
+
│ └── utils/ # Utility functions
|
| 106 |
+
│ ├── file_utils.py # File handling utilities
|
| 107 |
+
│ └── helpers.py # General helper functions
|
| 108 |
+
├── tests/ # Test suite
|
| 109 |
+
└── scripts/ # Utility scripts
|
| 110 |
```
|
| 111 |
|
| 112 |
+
## Configuration
|
| 113 |
|
| 114 |
+
The application uses environment-based configuration with Pydantic Settings. All configuration options are documented in `.env.example`.
|
| 115 |
|
| 116 |
+
### Key Configuration Sections
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
+
- **Application Settings**: Basic app configuration
|
| 119 |
+
- **Server Settings**: Host, port, and server options
|
| 120 |
+
- **Redis Settings**: Redis connection and caching configuration
|
| 121 |
+
- **Clerk Settings**: Authentication configuration
|
| 122 |
+
- **Security Settings**: JWT and security configuration
|
| 123 |
+
- **Logging Settings**: Structured logging configuration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
+
## Development
|
|
|
|
|
|
|
| 126 |
|
| 127 |
+
### Running Tests
|
| 128 |
|
| 129 |
```bash
|
| 130 |
+
# Run all tests
|
| 131 |
+
pytest
|
| 132 |
|
| 133 |
+
# Run with coverage
|
| 134 |
+
pytest --cov=src
|
| 135 |
|
| 136 |
+
# Run specific test file
|
| 137 |
+
pytest tests/test_main.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
```
|
| 139 |
|
| 140 |
+
### Code Quality
|
| 141 |
|
| 142 |
```bash
|
| 143 |
+
# Format code
|
| 144 |
+
black src/ tests/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
+
# Sort imports
|
| 147 |
+
isort src/ tests/
|
| 148 |
|
| 149 |
+
# Lint code
|
| 150 |
+
flake8 src/ tests/
|
| 151 |
|
| 152 |
+
# Type checking
|
| 153 |
+
mypy src/
|
| 154 |
+
```
|
|
|
|
| 155 |
|
| 156 |
+
### Development Scripts
|
| 157 |
|
| 158 |
+
- `python scripts/setup.py` - Set up development environment
|
| 159 |
+
- `python -m src.app.main` - Run development server
|
| 160 |
+
- `pytest` - Run test suite
|
|
|
|
| 161 |
|
| 162 |
+
## API Documentation
|
| 163 |
|
| 164 |
+
Once the application is running, you can access:
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
+
- **Swagger UI**: http://localhost:8000/docs
|
| 167 |
+
- **ReDoc**: http://localhost:8000/redoc
|
| 168 |
+
- **OpenAPI JSON**: http://localhost:8000/openapi.json
|
| 169 |
|
| 170 |
+
## Health Monitoring
|
| 171 |
|
| 172 |
+
The application includes built-in health check endpoints:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
- `GET /health` - Basic health status
|
| 175 |
+
- `GET /` - Root endpoint with basic information
|
|
|
|
|
|
|
| 176 |
|
| 177 |
+
## Logging
|
|
|
|
|
|
|
| 178 |
|
| 179 |
+
The application uses structured logging with configurable output formats:
|
|
|
|
|
|
|
| 180 |
|
| 181 |
+
- **Development**: Colorized console output
|
| 182 |
+
- **Production**: JSON-formatted logs
|
| 183 |
|
| 184 |
+
Log levels and formats can be configured via environment variables.
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
## Security
|
| 187 |
|
| 188 |
+
- **Authentication**: Clerk-based JWT authentication
|
| 189 |
+
- **CORS**: Configurable cross-origin resource sharing
|
| 190 |
+
- **Rate Limiting**: Built-in rate limiting support
|
| 191 |
+
- **Input Validation**: Pydantic-based request validation
|
| 192 |
+
- **Security Headers**: Automatic security header injection
|
| 193 |
|
| 194 |
+
## Contributing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
1. Fork the repository
|
| 197 |
2. Create a feature branch
|
| 198 |
3. Make your changes
|
| 199 |
+
4. Add tests for new functionality
|
| 200 |
+
5. Ensure all tests pass
|
| 201 |
+
6. Submit a pull request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
+
## License
|
| 204 |
|
| 205 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
README_API_TESTING.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# T2M API Testing Guide
|
| 2 |
+
|
| 3 |
+
This directory contains comprehensive test scripts for the T2M API endpoints.
|
| 4 |
+
|
| 5 |
+
## Files
|
| 6 |
+
|
| 7 |
+
- `test_api_endpoints.py` - Main test script with full endpoint coverage
|
| 8 |
+
- `run_api_tests.py` - Simple runner using configuration file
|
| 9 |
+
- `test_config.json` - Configuration file for API settings
|
| 10 |
+
- `requirements-test.txt` - Python dependencies for testing
|
| 11 |
+
|
| 12 |
+
## Quick Start
|
| 13 |
+
|
| 14 |
+
### 1. Install Dependencies
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
pip install -r requirements-test.txt
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
### 2. Configure API Settings
|
| 21 |
+
|
| 22 |
+
Edit `test_config.json` with your API details:
|
| 23 |
+
|
| 24 |
+
```json
|
| 25 |
+
{
|
| 26 |
+
"api_config": {
|
| 27 |
+
"base_url": "https://your-api-domain.com/api/v1",
|
| 28 |
+
"token": "your-actual-bearer-token"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### 3. Run Tests
|
| 34 |
+
|
| 35 |
+
**Option A: Using configuration file**
|
| 36 |
+
```bash
|
| 37 |
+
python run_api_tests.py
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
**Option B: Direct command line**
|
| 41 |
+
```bash
|
| 42 |
+
python test_api_endpoints.py --base-url https://your-api-domain.com/api/v1 --token your-token
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
**Option C: Token from file**
|
| 46 |
+
```bash
|
| 47 |
+
echo "your-token-here" > token.txt
|
| 48 |
+
python test_api_endpoints.py --base-url https://your-api-domain.com/api/v1 --token-file token.txt
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## Test Coverage
|
| 52 |
+
|
| 53 |
+
The test script covers all major API endpoints:
|
| 54 |
+
|
| 55 |
+
### 🔓 Public Endpoints (No Auth Required)
|
| 56 |
+
- `GET /auth/health` - Authentication service health
|
| 57 |
+
- `GET /system/health` - System health check
|
| 58 |
+
|
| 59 |
+
### 🔐 Authentication Endpoints
|
| 60 |
+
- `GET /auth/status` - Authentication status
|
| 61 |
+
- `GET /auth/profile` - User profile
|
| 62 |
+
- `GET /auth/permissions` - User permissions
|
| 63 |
+
- `GET /auth/test/protected` - Protected endpoint test
|
| 64 |
+
- `GET /auth/test/verified` - Verified user test
|
| 65 |
+
- `POST /auth/verify` - Token verification
|
| 66 |
+
|
| 67 |
+
### 📁 File Management Endpoints
|
| 68 |
+
- `POST /files/upload` - Single file upload
|
| 69 |
+
- `GET /files` - List files with pagination
|
| 70 |
+
- `GET /files/{id}` - File details
|
| 71 |
+
- `GET /files/{id}/metadata` - File metadata
|
| 72 |
+
- `GET /files/{id}/thumbnail` - File thumbnail
|
| 73 |
+
- `GET /files/stats` - File statistics
|
| 74 |
+
- `DELETE /files/{id}` - File deletion (cleanup)
|
| 75 |
+
|
| 76 |
+
### ⚙️ Job Management Endpoints
|
| 77 |
+
- `GET /jobs` - List jobs
|
| 78 |
+
- `GET /jobs/{id}` - Job details
|
| 79 |
+
- `GET /jobs/{id}/logs` - Job logs
|
| 80 |
+
- `POST /jobs/{id}/cancel` - Cancel job (cleanup)
|
| 81 |
+
- `DELETE /jobs/{id}` - Delete job (cleanup)
|
| 82 |
+
|
| 83 |
+
### 🖥️ System Monitoring Endpoints
|
| 84 |
+
- `GET /system/metrics` - System metrics
|
| 85 |
+
- `GET /system/queue` - Queue status
|
| 86 |
+
- `GET /system/cache` - Cache information
|
| 87 |
+
- `GET /system/cache/metrics` - Cache metrics
|
| 88 |
+
- `GET /system/cache/report` - Cache report
|
| 89 |
+
- `GET /system/performance` - Performance summary
|
| 90 |
+
- `GET /system/connections` - Connection statistics
|
| 91 |
+
- `GET /system/async` - Async statistics
|
| 92 |
+
- `GET /system/deduplication` - Deduplication statistics
|
| 93 |
+
|
| 94 |
+
### 🎥 Video Processing Endpoints
|
| 95 |
+
- `POST /videos/generate` - Generate video
|
| 96 |
+
- `GET /videos/{id}/status` - Video job status
|
| 97 |
+
- `GET /videos/{id}/metadata` - Video metadata
|
| 98 |
+
- `GET /videos/{id}/thumbnail` - Video thumbnail
|
| 99 |
+
|
| 100 |
+
## Test Results
|
| 101 |
+
|
| 102 |
+
After running tests, you'll get:
|
| 103 |
+
|
| 104 |
+
1. **Console output** with real-time test results
|
| 105 |
+
2. **Summary statistics** showing pass/fail rates
|
| 106 |
+
3. **JSON report** saved to `api_test_results.json`
|
| 107 |
+
|
| 108 |
+
### Example Output
|
| 109 |
+
|
| 110 |
+
```
|
| 111 |
+
🚀 Starting T2M API Endpoint Tests
|
| 112 |
+
Base URL: https://api.example.com/api/v1
|
| 113 |
+
Token: Provided
|
| 114 |
+
|
| 115 |
+
🔓 Testing Public Endpoints
|
| 116 |
+
✅ PASS GET /auth/health
|
| 117 |
+
Status: 200
|
| 118 |
+
✅ PASS GET /system/health
|
| 119 |
+
Status: 200
|
| 120 |
+
|
| 121 |
+
🔐 Testing Authentication Endpoints
|
| 122 |
+
✅ PASS GET /auth/status
|
| 123 |
+
Status: 200
|
| 124 |
+
✅ PASS GET /auth/profile
|
| 125 |
+
Status: 200
|
| 126 |
+
|
| 127 |
+
📊 TEST SUMMARY
|
| 128 |
+
==================================================
|
| 129 |
+
Total Tests: 25
|
| 130 |
+
Passed: 23 ✅
|
| 131 |
+
Failed: 2 ❌
|
| 132 |
+
Success Rate: 92.0%
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
## Test Features
|
| 136 |
+
|
| 137 |
+
### 🧹 Automatic Cleanup
|
| 138 |
+
- Deletes uploaded test files
|
| 139 |
+
- Cancels/deletes created jobs
|
| 140 |
+
- Prevents resource accumulation
|
| 141 |
+
|
| 142 |
+
### 📊 Comprehensive Reporting
|
| 143 |
+
- Real-time console feedback
|
| 144 |
+
- Detailed JSON results file
|
| 145 |
+
- Pass/fail statistics
|
| 146 |
+
- Error details for debugging
|
| 147 |
+
|
| 148 |
+
### 🔧 Flexible Configuration
|
| 149 |
+
- Command line arguments
|
| 150 |
+
- Configuration file support
|
| 151 |
+
- Token file support
|
| 152 |
+
- Environment variable support
|
| 153 |
+
|
| 154 |
+
### 🛡️ Error Handling
|
| 155 |
+
- Network timeout handling
|
| 156 |
+
- Graceful failure handling
|
| 157 |
+
- Detailed error reporting
|
| 158 |
+
- Resource cleanup on failure
|
| 159 |
+
|
| 160 |
+
## Advanced Usage
|
| 161 |
+
|
| 162 |
+
### Testing Specific Endpoints
|
| 163 |
+
|
| 164 |
+
You can modify the test script to focus on specific endpoint groups:
|
| 165 |
+
|
| 166 |
+
```python
|
| 167 |
+
# Only test public endpoints
|
| 168 |
+
tester.test_public_endpoints()
|
| 169 |
+
|
| 170 |
+
# Only test file operations
|
| 171 |
+
tester.test_file_endpoints()
|
| 172 |
+
|
| 173 |
+
# Only test system monitoring
|
| 174 |
+
tester.test_system_endpoints()
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### Custom Test Data
|
| 178 |
+
|
| 179 |
+
Modify `test_config.json` to customize test parameters:
|
| 180 |
+
|
| 181 |
+
```json
|
| 182 |
+
{
|
| 183 |
+
"test_data": {
|
| 184 |
+
"video_generation": {
|
| 185 |
+
"prompt": "Your custom test prompt",
|
| 186 |
+
"duration": 10,
|
| 187 |
+
"quality": "1080p"
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
### Environment Variables
|
| 194 |
+
|
| 195 |
+
You can also use environment variables:
|
| 196 |
+
|
| 197 |
+
```bash
|
| 198 |
+
export T2M_API_URL="https://your-api-domain.com/api/v1"
|
| 199 |
+
export T2M_API_TOKEN="your-token-here"
|
| 200 |
+
python test_api_endpoints.py --base-url $T2M_API_URL --token $T2M_API_TOKEN
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
## Troubleshooting
|
| 204 |
+
|
| 205 |
+
### Common Issues
|
| 206 |
+
|
| 207 |
+
**Connection Errors**
|
| 208 |
+
- Verify the base URL is correct
|
| 209 |
+
- Check network connectivity
|
| 210 |
+
- Ensure API server is running
|
| 211 |
+
|
| 212 |
+
**Authentication Errors**
|
| 213 |
+
- Verify token is valid and not expired
|
| 214 |
+
- Check token format (should be just the token, not "Bearer token")
|
| 215 |
+
- Ensure user has required permissions
|
| 216 |
+
|
| 217 |
+
**Rate Limiting**
|
| 218 |
+
- Tests may hit rate limits on busy servers
|
| 219 |
+
- Add delays between requests if needed
|
| 220 |
+
- Run tests during off-peak hours
|
| 221 |
+
|
| 222 |
+
**Resource Cleanup Failures**
|
| 223 |
+
- Some resources may not be cleaned up if tests fail
|
| 224 |
+
- Manually delete test files/jobs if needed
|
| 225 |
+
- Check API logs for cleanup issues
|
| 226 |
+
|
| 227 |
+
### Debug Mode
|
| 228 |
+
|
| 229 |
+
For more detailed debugging, modify the test script to add verbose logging:
|
| 230 |
+
|
| 231 |
+
```python
|
| 232 |
+
import logging
|
| 233 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
## Integration with CI/CD
|
| 237 |
+
|
| 238 |
+
The test script returns appropriate exit codes for CI/CD integration:
|
| 239 |
+
|
| 240 |
+
```bash
|
| 241 |
+
# Run tests and capture exit code
|
| 242 |
+
python test_api_endpoints.py --base-url $API_URL --token $API_TOKEN
|
| 243 |
+
if [ $? -eq 0 ]; then
|
| 244 |
+
echo "All tests passed"
|
| 245 |
+
else
|
| 246 |
+
echo "Some tests failed"
|
| 247 |
+
exit 1
|
| 248 |
+
fi
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
## Contributing
|
| 252 |
+
|
| 253 |
+
To add new test cases:
|
| 254 |
+
|
| 255 |
+
1. Add the endpoint to the appropriate test method
|
| 256 |
+
2. Follow the existing pattern for error handling
|
| 257 |
+
3. Add cleanup logic if the endpoint creates resources
|
| 258 |
+
4. Update this README with the new endpoint coverage
|
SYSTEM_OVERVIEW.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-Agent Video Generation System - Architecture Overview
|
| 2 |
+
|
| 3 |
+
## 🎯 System Purpose
|
| 4 |
+
This is a sophisticated **multi-agent system** that automatically generates educational videos using Manim (Mathematical Animation Engine). The system transforms textual descriptions of mathematical concepts, theorems, and educational content into high-quality animated videos through coordinated AI agents.
|
| 5 |
+
|
| 6 |
+
## 🏗️ System Architecture
|
| 7 |
+
|
| 8 |
+
```mermaid
|
| 9 |
+
flowchart TD
|
| 10 |
+
%% Input Layer
|
| 11 |
+
U["User Input<br/>(Topic & Context)"]:::input
|
| 12 |
+
GV["generate_video.py<br/>(Main Orchestrator)"]:::input
|
| 13 |
+
ES["evaluate.py<br/>(Quality Assessment)"]:::input
|
| 14 |
+
|
| 15 |
+
%% Configuration and Data
|
| 16 |
+
CONF["Configuration<br/>(.env, src/config)"]:::config
|
| 17 |
+
DATA["Data Repository<br/>(data/)"]:::data
|
| 18 |
+
|
| 19 |
+
%% Core Generation Pipeline
|
| 20 |
+
subgraph "Core Multi-Agent Pipeline"
|
| 21 |
+
CG["Code Generation Agent<br/>(src/core/code_generator.py)"]:::core
|
| 22 |
+
VP["Video Planning Agent<br/>(src/core/video_planner.py)"]:::core
|
| 23 |
+
VR["Video Rendering Agent<br/>(src/core/video_renderer.py)"]:::core
|
| 24 |
+
end
|
| 25 |
+
|
| 26 |
+
%% Retrieval & Augmentation (RAG)
|
| 27 |
+
RAG["RAG Intelligence Agent<br/>(src/rag/rag_integration.py,<br/>src/rag/vector_store.py)"]:::rag
|
| 28 |
+
|
| 29 |
+
%% Task & Prompt Generation
|
| 30 |
+
TASK["Task & Prompt Generation<br/>(task_generator/)"]:::task
|
| 31 |
+
|
| 32 |
+
%% External LLM & Model Tools
|
| 33 |
+
LLM["LLM Provider Agents<br/>(mllm_tools/)"]:::ai
|
| 34 |
+
|
| 35 |
+
%% Voiceover & Utilities
|
| 36 |
+
VOX["Utility Services<br/>(src/utils/)"]:::voice
|
| 37 |
+
|
| 38 |
+
%% Evaluation Module
|
| 39 |
+
EVAL["Quality Evaluation Agent<br/>(eval_suite/)"]:::eval
|
| 40 |
+
|
| 41 |
+
%% Connections
|
| 42 |
+
U -->|"provides data"| GV
|
| 43 |
+
GV -->|"reads configuration"| CONF
|
| 44 |
+
CONF -->|"configures processing"| CG
|
| 45 |
+
CONF -->|"fetches theorem data"| DATA
|
| 46 |
+
|
| 47 |
+
%% Core Pipeline Flow
|
| 48 |
+
GV -->|"orchestrates generation"| CG
|
| 49 |
+
CG -->|"sends code/instructions"| VP
|
| 50 |
+
VP -->|"plans scenes"| VR
|
| 51 |
+
VR -->|"integrates audio"| VOX
|
| 52 |
+
VOX -->|"produces final video"| EVAL
|
| 53 |
+
|
| 54 |
+
%% Cross Module Integrations
|
| 55 |
+
TASK -->|"supplies prompt templates"| CG
|
| 56 |
+
TASK -->|"guides scene planning"| VP
|
| 57 |
+
CG -->|"augments with retrieval"| RAG
|
| 58 |
+
VP -->|"queries documentation"| RAG
|
| 59 |
+
LLM -->|"supports AI generation"| CG
|
| 60 |
+
LLM -->|"supports task generation"| TASK
|
| 61 |
+
|
| 62 |
+
%% Evaluation Script
|
| 63 |
+
ES -->|"evaluates output"| EVAL
|
| 64 |
+
|
| 65 |
+
%% Styles
|
| 66 |
+
classDef input fill:#FFD580,stroke:#333,stroke-width:2px;
|
| 67 |
+
classDef config fill:#B3E5FC,stroke:#333,stroke-width:2px;
|
| 68 |
+
classDef data fill:#C8E6C9,stroke:#333,stroke-width:2px;
|
| 69 |
+
classDef core fill:#FFF59D,stroke:#333,stroke-width:2px;
|
| 70 |
+
classDef rag fill:#FFCC80,stroke:#333,stroke-width:2px;
|
| 71 |
+
classDef task fill:#D1C4E9,stroke:#333,stroke-width:2px;
|
| 72 |
+
classDef ai fill:#B2EBF2,stroke:#333,stroke-width:2px;
|
| 73 |
+
classDef voice fill:#FFE0B2,stroke:#333,stroke-width:2px;
|
| 74 |
+
classDef eval fill:#E1BEE7,stroke:#333,stroke-width:2px;
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## 🤖 Core Agents & Responsibilities
|
| 78 |
+
|
| 79 |
+
### 1. **🎬 Video Planning Agent** (`src/core/video_planner.py`)
|
| 80 |
+
**Role**: Strategic planning and scene orchestration
|
| 81 |
+
|
| 82 |
+
**Key Capabilities**:
|
| 83 |
+
- Scene outline generation and decomposition
|
| 84 |
+
- Storyboard creation with visual descriptions
|
| 85 |
+
- Technical implementation planning
|
| 86 |
+
- Concurrent scene processing with enhanced parallelization
|
| 87 |
+
- Context learning from previous examples
|
| 88 |
+
- RAG integration for Manim documentation retrieval
|
| 89 |
+
|
| 90 |
+
**Key Methods**:
|
| 91 |
+
- `generate_scene_outline()` - Creates overall video structure
|
| 92 |
+
- `generate_scene_implementation_concurrently_enhanced()` - Parallel scene planning
|
| 93 |
+
- `_initialize_context_examples()` - Loads learning contexts
|
| 94 |
+
|
| 95 |
+
### 2. **⚡ Code Generation Agent** (`src/core/code_generator.py`)
|
| 96 |
+
**Role**: Manim code synthesis and optimization
|
| 97 |
+
|
| 98 |
+
**Key Capabilities**:
|
| 99 |
+
- Intelligent Manim code generation from scene descriptions
|
| 100 |
+
- Automatic error detection and fixing
|
| 101 |
+
- Visual self-reflection for code quality
|
| 102 |
+
- RAG-enhanced code generation with documentation context
|
| 103 |
+
- Context learning from successful examples
|
| 104 |
+
- Banned reasoning prevention
|
| 105 |
+
|
| 106 |
+
**Key Methods**:
|
| 107 |
+
- `generate_manim_code()` - Primary code generation
|
| 108 |
+
- `fix_code_errors()` - Intelligent error correction
|
| 109 |
+
- `visual_self_reflection()` - Quality validation
|
| 110 |
+
|
| 111 |
+
### 3. **🎞️ Video Rendering Agent** (`src/core/video_renderer.py`)
|
| 112 |
+
**Role**: Video compilation and optimization
|
| 113 |
+
|
| 114 |
+
**Key Capabilities**:
|
| 115 |
+
- Optimized Manim scene rendering
|
| 116 |
+
- Intelligent caching system for performance
|
| 117 |
+
- Parallel scene processing
|
| 118 |
+
- Quality preset management (preview/low/medium/high/production)
|
| 119 |
+
- GPU acceleration support
|
| 120 |
+
- Video combination and assembly
|
| 121 |
+
|
| 122 |
+
**Key Methods**:
|
| 123 |
+
- `render_scene_optimized()` - Enhanced scene rendering
|
| 124 |
+
- `combine_videos_optimized()` - Final video assembly
|
| 125 |
+
- `_get_code_hash()` - Intelligent caching
|
| 126 |
+
|
| 127 |
+
### 4. **🔍 RAG Intelligence Agent** (`src/rag/rag_integration.py`, `src/rag/vector_store.py`)
|
| 128 |
+
**Role**: Knowledge retrieval and context augmentation
|
| 129 |
+
|
| 130 |
+
**Key Capabilities**:
|
| 131 |
+
- Manim documentation retrieval
|
| 132 |
+
- Plugin detection and relevance scoring
|
| 133 |
+
- Vector store management with ChromaDB
|
| 134 |
+
- Query generation for technical contexts
|
| 135 |
+
- Enhanced document embedding and retrieval
|
| 136 |
+
|
| 137 |
+
**Key Methods**:
|
| 138 |
+
- `detect_relevant_plugins()` - Smart plugin identification
|
| 139 |
+
- `retrieve_relevant_docs()` - Context-aware documentation retrieval
|
| 140 |
+
- `generate_rag_queries()` - Intelligent query formulation
|
| 141 |
+
|
| 142 |
+
### 5. **📝 Task & Prompt Generation Service** (`task_generator/`)
|
| 143 |
+
**Role**: Template management and prompt engineering
|
| 144 |
+
|
| 145 |
+
**Key Capabilities**:
|
| 146 |
+
- Dynamic prompt template generation
|
| 147 |
+
- Context-aware prompt customization
|
| 148 |
+
- Banned reasoning pattern management
|
| 149 |
+
- Multi-modal prompt support
|
| 150 |
+
|
| 151 |
+
**Key Components**:
|
| 152 |
+
- `parse_prompt.py` - Template processing
|
| 153 |
+
- `prompts_raw/` - Prompt template repository
|
| 154 |
+
|
| 155 |
+
### 6. **🤖 LLM Provider Agents** (`mllm_tools/`)
|
| 156 |
+
**Role**: AI model abstraction and management
|
| 157 |
+
|
| 158 |
+
**Key Capabilities**:
|
| 159 |
+
- Multi-provider LLM support (OpenAI, Gemini, Vertex AI, OpenRouter)
|
| 160 |
+
- Unified interface for different AI models
|
| 161 |
+
- Cost tracking and usage monitoring
|
| 162 |
+
- Langfuse integration for observability
|
| 163 |
+
|
| 164 |
+
**Key Components**:
|
| 165 |
+
- `litellm.py` - LiteLLM wrapper for multiple providers
|
| 166 |
+
- `openrouter.py` - OpenRouter integration
|
| 167 |
+
- `gemini.py` - Google Gemini integration
|
| 168 |
+
- `vertex_ai.py` - Google Cloud Vertex AI
|
| 169 |
+
|
| 170 |
+
### 7. **✅ Quality Evaluation Agent** (`eval_suite/`)
|
| 171 |
+
**Role**: Output validation and quality assurance
|
| 172 |
+
|
| 173 |
+
**Key Capabilities**:
|
| 174 |
+
- Multi-modal content evaluation (text, image, video)
|
| 175 |
+
- Automated quality scoring
|
| 176 |
+
- Error pattern detection
|
| 177 |
+
- Performance metrics collection
|
| 178 |
+
|
| 179 |
+
**Key Components**:
|
| 180 |
+
- `text_utils.py` - Text quality evaluation
|
| 181 |
+
- `image_utils.py` - Visual content assessment
|
| 182 |
+
- `video_utils.py` - Video quality metrics
|
| 183 |
+
|
| 184 |
+
## 🔄 Multi-Agent Workflow
|
| 185 |
+
|
| 186 |
+
### **Phase 1: Initialization & Planning**
|
| 187 |
+
1. **System Orchestrator** (`generate_video.py`) receives user input
|
| 188 |
+
2. **Configuration Manager** loads system settings and model configurations
|
| 189 |
+
3. **Session Manager** creates/loads session for continuity
|
| 190 |
+
4. **Video Planning Agent** analyzes topic and creates scene breakdown
|
| 191 |
+
5. **RAG Agent** detects relevant plugins and retrieves documentation
|
| 192 |
+
|
| 193 |
+
### **Phase 2: Implementation Planning**
|
| 194 |
+
1. **Video Planning Agent** generates detailed implementation plans for each scene
|
| 195 |
+
2. **Task Generator** provides appropriate prompt templates
|
| 196 |
+
3. **RAG Agent** augments plans with relevant technical documentation
|
| 197 |
+
4. **Scene Analyzer** validates plan completeness
|
| 198 |
+
|
| 199 |
+
### **Phase 3: Code Generation**
|
| 200 |
+
1. **Code Generation Agent** transforms scene plans into Manim code
|
| 201 |
+
2. **RAG Agent** provides contextual documentation for complex animations
|
| 202 |
+
3. **Error Detection** validates code syntax and logic
|
| 203 |
+
4. **Quality Assurance** ensures code meets standards
|
| 204 |
+
|
| 205 |
+
### **Phase 4: Rendering & Assembly**
|
| 206 |
+
1. **Video Rendering Agent** executes Manim code to generate scenes
|
| 207 |
+
2. **Caching System** optimizes performance through intelligent storage
|
| 208 |
+
3. **Parallel Processing** renders multiple scenes concurrently
|
| 209 |
+
4. **Quality Control** validates rendered output
|
| 210 |
+
|
| 211 |
+
### **Phase 5: Final Assembly**
|
| 212 |
+
1. **Video Rendering Agent** combines individual scenes
|
| 213 |
+
2. **Audio Integration** adds voiceovers and sound effects
|
| 214 |
+
3. **Quality Evaluation Agent** performs final validation
|
| 215 |
+
4. **Output Manager** delivers final video with metadata
|
| 216 |
+
|
| 217 |
+
## 🏛️ Design Principles
|
| 218 |
+
|
| 219 |
+
### **SOLID Principles Implementation**
|
| 220 |
+
|
| 221 |
+
1. **Single Responsibility Principle**
|
| 222 |
+
- Each agent has a focused, well-defined purpose
|
| 223 |
+
- Clear separation of concerns across components
|
| 224 |
+
|
| 225 |
+
2. **Open/Closed Principle**
|
| 226 |
+
- System extensible through composition and interfaces
|
| 227 |
+
- New agents can be added without modifying existing code
|
| 228 |
+
|
| 229 |
+
3. **Liskov Substitution Principle**
|
| 230 |
+
- Agents implement common interfaces for interchangeability
|
| 231 |
+
- Protocol-based design ensures compatibility
|
| 232 |
+
|
| 233 |
+
4. **Interface Segregation Principle**
|
| 234 |
+
- Clean, focused interfaces for agent communication
|
| 235 |
+
- No forced dependencies on unused functionality
|
| 236 |
+
|
| 237 |
+
5. **Dependency Inversion Principle**
|
| 238 |
+
- High-level modules depend on abstractions
|
| 239 |
+
- Factory pattern for component creation
|
| 240 |
+
|
| 241 |
+
### **Multi-Agent Coordination Patterns**
|
| 242 |
+
|
| 243 |
+
1. **Pipeline Architecture**: Sequential processing with clear handoffs
|
| 244 |
+
2. **Publish-Subscribe**: Event-driven communication between agents
|
| 245 |
+
3. **Factory Pattern**: Dynamic agent creation and configuration
|
| 246 |
+
4. **Strategy Pattern**: Pluggable algorithms for different tasks
|
| 247 |
+
5. **Observer Pattern**: Monitoring and logging across agents
|
| 248 |
+
|
| 249 |
+
## ⚡ Performance Optimizations
|
| 250 |
+
|
| 251 |
+
### **Concurrency & Parallelization**
|
| 252 |
+
- **Async/Await**: Non-blocking agent coordination
|
| 253 |
+
- **Semaphore Control**: Intelligent resource management
|
| 254 |
+
- **Thread Pools**: Parallel I/O operations
|
| 255 |
+
- **Concurrent Scene Processing**: Multiple scenes rendered simultaneously
|
| 256 |
+
|
| 257 |
+
### **Intelligent Caching**
|
| 258 |
+
- **Code Hash-based Caching**: Avoid redundant renders
|
| 259 |
+
- **Context Caching**: Reuse prompt templates and examples
|
| 260 |
+
- **Vector Store Caching**: Optimized document retrieval
|
| 261 |
+
|
| 262 |
+
### **Resource Management**
|
| 263 |
+
- **GPU Acceleration**: Hardware-accelerated rendering
|
| 264 |
+
- **Memory Optimization**: Efficient data structures
|
| 265 |
+
- **Quality Presets**: Speed vs. quality tradeoffs
|
| 266 |
+
|
| 267 |
+
## 🔧 Configuration Management
|
| 268 |
+
|
| 269 |
+
### **Environment Configuration** (`.env`, `src/config/config.py`)
|
| 270 |
+
```python
|
| 271 |
+
class VideoGenerationConfig:
|
| 272 |
+
planner_model: str # Primary AI model
|
| 273 |
+
scene_model: Optional[str] = None # Scene-specific model
|
| 274 |
+
helper_model: Optional[str] = None # Helper tasks model
|
| 275 |
+
max_scene_concurrency: int = 5 # Parallel scene limit
|
| 276 |
+
use_rag: bool = False # RAG integration
|
| 277 |
+
enable_caching: bool = True # Performance caching
|
| 278 |
+
use_gpu_acceleration: bool = False # Hardware acceleration
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
### **Model Provider Configuration**
|
| 282 |
+
- Support for multiple LLM providers (OpenAI, Gemini, Claude, etc.)
|
| 283 |
+
- Unified interface through LiteLLM
|
| 284 |
+
- Cost tracking and usage monitoring
|
| 285 |
+
- Automatic failover capabilities
|
| 286 |
+
|
| 287 |
+
## 📊 Data Flow Architecture
|
| 288 |
+
|
| 289 |
+
### **Input Data Sources**
|
| 290 |
+
- **Theorem Datasets**: JSON files with mathematical concepts (`data/thb_*/`)
|
| 291 |
+
- **Context Learning**: Historical examples (`data/context_learning/`)
|
| 292 |
+
- **RAG Documentation**: Manim docs and plugins (`data/rag/manim_docs/`)
|
| 293 |
+
|
| 294 |
+
### **Processing Pipeline**
|
| 295 |
+
```
|
| 296 |
+
User Input → Topic Analysis → Scene Planning → Code Generation → Rendering → Quality Check → Final Output
|
| 297 |
+
↓ ↓ ↓ ↓ ↓ ↓
|
| 298 |
+
Configuration → RAG Context → Implementation → Error Fixing → Optimization → Validation
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
### **Output Artifacts**
|
| 302 |
+
- **Scene Outlines**: Structured video plans
|
| 303 |
+
- **Implementation Plans**: Technical specifications
|
| 304 |
+
- **Manim Code**: Executable animation scripts
|
| 305 |
+
- **Rendered Videos**: Individual scene outputs
|
| 306 |
+
- **Combined Videos**: Final assembled content
|
| 307 |
+
- **Metadata**: Processing logs and metrics
|
| 308 |
+
|
| 309 |
+
## 🎪 Advanced Features
|
| 310 |
+
|
| 311 |
+
### **Error Recovery & Self-Healing**
|
| 312 |
+
- **Multi-layer Retry Logic**: Automatic error recovery at each agent level
|
| 313 |
+
- **Intelligent Error Analysis**: Pattern recognition for common failures
|
| 314 |
+
- **Self-Reflection**: Code quality validation through visual analysis
|
| 315 |
+
- **Fallback Strategies**: Alternative approaches when primary methods fail
|
| 316 |
+
|
| 317 |
+
### **Monitoring & Observability**
|
| 318 |
+
- **Langfuse Integration**: Comprehensive LLM call tracking
|
| 319 |
+
- **Performance Metrics**: Render times, success rates, resource usage
|
| 320 |
+
- **Status Dashboard**: Real-time pipeline state visualization
|
| 321 |
+
- **Cost Tracking**: Token usage and API cost monitoring
|
| 322 |
+
|
| 323 |
+
### **Scalability Features**
|
| 324 |
+
- **Horizontal Scaling**: Multiple concurrent topic processing
|
| 325 |
+
- **Resource Pooling**: Shared computational resources
|
| 326 |
+
- **Load Balancing**: Intelligent task distribution
|
| 327 |
+
- **State Persistence**: Resume interrupted processing
|
| 328 |
+
|
| 329 |
+
## 🚀 Usage Examples
|
| 330 |
+
|
| 331 |
+
### **Single Topic Generation**
|
| 332 |
+
```bash
|
| 333 |
+
python generate_video.py \
|
| 334 |
+
--topic "Pythagorean Theorem" \
|
| 335 |
+
--context "Explain the mathematical proof and visual demonstration" \
|
| 336 |
+
--model "gemini/gemini-2.5-flash-preview-04-17" \
|
| 337 |
+
--use_rag \
|
| 338 |
+
--quality medium
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
### **Batch Processing**
|
| 342 |
+
```bash
|
| 343 |
+
python generate_video.py \
|
| 344 |
+
--theorems_path data/thb_easy/math.json \
|
| 345 |
+
--sample_size 5 \
|
| 346 |
+
--max_scene_concurrency 3 \
|
| 347 |
+
--use_context_learning \
|
| 348 |
+
--enable_caching
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
### **Status Monitoring**
|
| 352 |
+
```bash
|
| 353 |
+
python generate_video.py \
|
| 354 |
+
--theorems_path data/thb_easy/math.json \
|
| 355 |
+
--check_status
|
| 356 |
+
```
|
| 357 |
+
|
| 358 |
+
## 📈 System Metrics & KPIs
|
| 359 |
+
|
| 360 |
+
### **Performance Indicators**
|
| 361 |
+
- **Scene Generation Speed**: Average time per scene
|
| 362 |
+
- **Rendering Efficiency**: Cache hit rates and parallel utilization
|
| 363 |
+
- **Quality Scores**: Automated evaluation metrics
|
| 364 |
+
- **Success Rates**: Completion percentage across pipeline stages
|
| 365 |
+
|
| 366 |
+
### **Resource Utilization**
|
| 367 |
+
- **LLM Token Usage**: Cost optimization and efficiency
|
| 368 |
+
- **Computational Resources**: CPU/GPU utilization
|
| 369 |
+
- **Storage Efficiency**: Cache effectiveness and data management
|
| 370 |
+
- **Memory Footprint**: System resource consumption
|
| 371 |
+
|
| 372 |
+
## 🔮 Future Enhancements
|
| 373 |
+
|
| 374 |
+
### **Planned Agent Improvements**
|
| 375 |
+
- **Advanced Visual Agent**: Enhanced image understanding and generation
|
| 376 |
+
- **Audio Synthesis Agent**: Dynamic voiceover generation
|
| 377 |
+
- **Interactive Agent**: Real-time user feedback integration
|
| 378 |
+
- **Curriculum Agent**: Adaptive learning path generation
|
| 379 |
+
|
| 380 |
+
### **Technical Roadmap**
|
| 381 |
+
- **Distributed Processing**: Multi-node agent deployment
|
| 382 |
+
- **Real-time Streaming**: Live video generation capabilities
|
| 383 |
+
- **Mobile Integration**: Responsive design for mobile platforms
|
| 384 |
+
- **API Gateway**: RESTful service architecture
|
| 385 |
+
|
| 386 |
+
---
|
| 387 |
+
|
| 388 |
+
## 📚 Related Documentation
|
| 389 |
+
|
| 390 |
+
- **[API Reference](docs/api_reference.md)** - Detailed method documentation
|
| 391 |
+
- **[Configuration Guide](docs/configuration.md)** - Setup and customization
|
| 392 |
+
- **[Development Guide](docs/development.md)** - Contributing and extending
|
| 393 |
+
- **[Troubleshooting](docs/troubleshooting.md)** - Common issues and solutions
|
| 394 |
+
|
| 395 |
+
---
|
| 396 |
+
|
| 397 |
+
**Last Updated**: August 25, 2025
|
| 398 |
+
**Version**: Multi-Agent Enhanced Pipeline v2.0
|
| 399 |
+
**Maintainer**: T2M Development Team
|
WORKING_SETUP.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ✅ Working FastAPI + Docker + ngrok Setup
|
| 2 |
+
|
| 3 |
+
## 🎯 Current Status: WORKING!
|
| 4 |
+
|
| 5 |
+
Your development environment is successfully set up with:
|
| 6 |
+
- ✅ Redis running in Docker
|
| 7 |
+
- ✅ ngrok exposing your local server publicly
|
| 8 |
+
- ✅ FastAPI server running locally
|
| 9 |
+
- ✅ Public HTTPS URL: `https://d4e9601ecb72.ngrok-free.app`
|
| 10 |
+
|
| 11 |
+
## 🚀 Quick Start Commands
|
| 12 |
+
|
| 13 |
+
### 1. Start Services (Redis + ngrok)
|
| 14 |
+
```bash
|
| 15 |
+
./start-services.sh
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
### 2. Start FastAPI Server
|
| 19 |
+
```bash
|
| 20 |
+
# Simple working version
|
| 21 |
+
python simple_app.py
|
| 22 |
+
|
| 23 |
+
# Or the full version (once config is fixed)
|
| 24 |
+
python -m uvicorn src.app.main:app --reload --host 0.0.0.0 --port 8000
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### 3. Get Your Public URL
|
| 28 |
+
```bash
|
| 29 |
+
curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url'
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## 🌐 Your URLs
|
| 33 |
+
|
| 34 |
+
- **Local API**: http://localhost:8000
|
| 35 |
+
- **Public API**: https://d4e9601ecb72.ngrok-free.app
|
| 36 |
+
- **API Docs**: https://d4e9601ecb72.ngrok-free.app/docs (when using simple_app.py)
|
| 37 |
+
- **ngrok Dashboard**: http://localhost:4040
|
| 38 |
+
- **Redis**: localhost:6379
|
| 39 |
+
|
| 40 |
+
## 🔧 Configure Clerk Webhook
|
| 41 |
+
|
| 42 |
+
Now you can set up your Clerk webhook:
|
| 43 |
+
|
| 44 |
+
1. Go to [Clerk Dashboard](https://dashboard.clerk.com/)
|
| 45 |
+
2. Navigate to **Webhooks** → **Add Endpoint**
|
| 46 |
+
3. Enter your webhook URL:
|
| 47 |
+
```
|
| 48 |
+
https://d4e9601ecb72.ngrok-free.app/api/v1/auth/webhooks/clerk
|
| 49 |
+
```
|
| 50 |
+
4. Select events: `user.created`, `user.updated`, `user.deleted`, `session.created`, `session.ended`
|
| 51 |
+
5. Copy the **Signing Secret** and add to your `.env`:
|
| 52 |
+
```env
|
| 53 |
+
CLERK_WEBHOOK_SECRET=whsec_your_webhook_signing_secret_here
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## 📝 Available Endpoints (Simple App)
|
| 57 |
+
|
| 58 |
+
- `GET /` - Welcome message
|
| 59 |
+
- `GET /health` - Health check
|
| 60 |
+
- `GET /config` - Configuration info
|
| 61 |
+
|
| 62 |
+
Test them:
|
| 63 |
+
```bash
|
| 64 |
+
curl https://d4e9601ecb72.ngrok-free.app/
|
| 65 |
+
curl https://d4e9601ecb72.ngrok-free.app/health
|
| 66 |
+
curl https://d4e9601ecb72.ngrok-free.app/config
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## 🛠️ Service Management
|
| 70 |
+
|
| 71 |
+
### Check Services
|
| 72 |
+
```bash
|
| 73 |
+
# Check Redis
|
| 74 |
+
redis-cli ping
|
| 75 |
+
|
| 76 |
+
# Check ngrok tunnels
|
| 77 |
+
curl -s http://localhost:4040/api/tunnels
|
| 78 |
+
|
| 79 |
+
# Check Docker containers
|
| 80 |
+
docker ps
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Stop Services
|
| 84 |
+
```bash
|
| 85 |
+
# Stop all related containers
|
| 86 |
+
docker stop $(docker ps -q --filter 'ancestor=redis:7-alpine' --filter 'ancestor=ngrok/ngrok:latest')
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Restart Services
|
| 90 |
+
```bash
|
| 91 |
+
./start-services.sh
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## 🔍 Troubleshooting
|
| 95 |
+
|
| 96 |
+
### If ngrok URL changes:
|
| 97 |
+
The ngrok URL will change each time you restart ngrok (unless you have a paid plan). Get the new URL with:
|
| 98 |
+
```bash
|
| 99 |
+
curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url'
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### If Redis connection fails:
|
| 103 |
+
```bash
|
| 104 |
+
# Check if Redis is running
|
| 105 |
+
docker ps | grep redis
|
| 106 |
+
|
| 107 |
+
# Restart Redis if needed
|
| 108 |
+
docker restart $(docker ps -q --filter 'ancestor=redis:7-alpine')
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### If FastAPI config fails:
|
| 112 |
+
Use the simple app for now:
|
| 113 |
+
```bash
|
| 114 |
+
python simple_app.py
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## 🎉 Next Steps
|
| 118 |
+
|
| 119 |
+
1. **Test your webhook**: Use the public URL to set up Clerk webhooks
|
| 120 |
+
2. **Fix the main FastAPI app**: The config parsing issue needs to be resolved
|
| 121 |
+
3. **Add authentication**: Implement Clerk authentication middleware
|
| 122 |
+
4. **Add your business logic**: Build your video generation endpoints
|
| 123 |
+
|
| 124 |
+
## 📋 Working Files
|
| 125 |
+
|
| 126 |
+
- `start-services.sh` - Starts Redis and ngrok
|
| 127 |
+
- `simple_app.py` - Working FastAPI application
|
| 128 |
+
- `simple_config.py` - Working configuration
|
| 129 |
+
- `.env` - Environment variables (configured)
|
| 130 |
+
|
| 131 |
+
Your development environment is ready for webhook testing! 🚀
|
auth_token.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDExMUFBQSIsImtpZCI6Imluc18zMXBCZktEanhrSFJDUUFUTDN6Q29pUmxad2YiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL3BvZXRpYy1wcmltYXRlLTQ4LmFjY291bnRzLmRldiIsImV4cCI6MTc1NjI3NzQ3OCwiZnZhIjpbMTcsLTFdLCJpYXQiOjE3NTYyNzc0MTgsImlzcyI6Imh0dHBzOi8vcG9ldGljLXByaW1hdGUtNDguY2xlcmsuYWNjb3VudHMuZGV2IiwibmJmIjoxNzU2Mjc3NDA4LCJzaWQiOiJzZXNzXzMxckpnS3R1Z1BKU2VlNVFJRXp5NGpYcW1NbyIsInN0cyI6ImFjdGl2ZSIsInN1YiI6InVzZXJfMzFxbHNuWDZYZTh4TmRXYnNHTDVNU3hnQnhIIiwidiI6Mn0.XUCI_plOZmMBfZhMMHViCj4KXpMWobjQp-AJf7VgdeALSi6lPKdzKA6vDLyjFngnY-XrFCDP4UI7iNNqRw32_Mvr9ipxqzWKAtL6P_KP3HonOEGfrsMibFUZdUF2cv9N3aJ-sL64QteIV0-NAdlprjM_vbYiW8dlJPuNlpOoFF9fWGPr8s3uVwEK6BPSrzbBSkcIHSbAGiBpzRaFdaupOZBgC-WAxj-_cPQIA0AKr_9nrG-UFozjdfbi74AGM0MWSP7GLe_pDQ-t1FKAFXMNtS0ZB5ylwnZpnAnxPBMG4UPQzHXjo5xeibtRb0-4JSUu-_kCmvTxWuvcHqsjqFwXpg
|
docker-compose.dev.yml
ADDED
|
File without changes
|
docker-compose.simple.yml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
# Redis service for caching and job queuing
|
| 5 |
+
redis:
|
| 6 |
+
image: redis:7-alpine
|
| 7 |
+
container_name: t2m-redis
|
| 8 |
+
ports:
|
| 9 |
+
- "6379:6379"
|
| 10 |
+
volumes:
|
| 11 |
+
- redis_data:/data
|
| 12 |
+
command: redis-server --appendonly yes
|
| 13 |
+
healthcheck:
|
| 14 |
+
test: ["CMD", "redis-cli", "ping"]
|
| 15 |
+
interval: 10s
|
| 16 |
+
timeout: 3s
|
| 17 |
+
retries: 3
|
| 18 |
+
restart: unless-stopped
|
| 19 |
+
|
| 20 |
+
# ngrok service - simple HTTP tunnel
|
| 21 |
+
ngrok:
|
| 22 |
+
image: ngrok/ngrok:latest
|
| 23 |
+
container_name: t2m-ngrok
|
| 24 |
+
restart: unless-stopped
|
| 25 |
+
command: ["http", "host.docker.internal:8000"]
|
| 26 |
+
ports:
|
| 27 |
+
- "4040:4040"
|
| 28 |
+
environment:
|
| 29 |
+
- NGROK_AUTHTOKEN=${NGROK_AUTHTOKEN}
|
| 30 |
+
extra_hosts:
|
| 31 |
+
- "host.docker.internal:host-gateway"
|
| 32 |
+
|
| 33 |
+
volumes:
|
| 34 |
+
redis_data:
|
docker-compose.test.yml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
theoremexplain:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: dockerfile
|
| 8 |
+
container_name: theoremexplain-agent
|
| 9 |
+
ports:
|
| 10 |
+
- "7860:7860"
|
| 11 |
+
volumes:
|
| 12 |
+
# Mount output directory to persist generated videos
|
| 13 |
+
- ./output:/app/output
|
| 14 |
+
# Mount models directory if you want to use local models
|
| 15 |
+
- ./models:/app/models
|
| 16 |
+
# Mount data directory for RAG and datasets
|
| 17 |
+
- ./data:/app/data
|
| 18 |
+
environment:
|
| 19 |
+
# Copy environment variables from host .env file
|
| 20 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 21 |
+
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
| 22 |
+
# Kokoro TTS settings
|
| 23 |
+
- KOKORO_MODEL_PATH=models/kokoro-v0_19.onnx
|
| 24 |
+
- KOKORO_VOICES_PATH=models/voices.bin
|
| 25 |
+
- KOKORO_DEFAULT_VOICE=af
|
| 26 |
+
- KOKORO_DEFAULT_SPEED=1.0
|
| 27 |
+
- KOKORO_DEFAULT_LANG=en-us
|
| 28 |
+
# Python path
|
| 29 |
+
- PYTHONPATH=/app:$PYTHONPATH
|
| 30 |
+
restart: unless-stopped
|
| 31 |
+
healthcheck:
|
| 32 |
+
test: ["uv", "run" , "manim" ,"checkhealth"]
|
| 33 |
+
interval: 30s
|
| 34 |
+
timeout: 10s
|
| 35 |
+
retries: 3
|
| 36 |
+
start_period: 60s
|
| 37 |
+
|
| 38 |
+
# Optional: Add a service for running batch generation
|
| 39 |
+
theoremexplain-batch:
|
| 40 |
+
build:
|
| 41 |
+
context: .
|
| 42 |
+
dockerfile: dockerfile
|
| 43 |
+
container_name: theoremexplain-batch
|
| 44 |
+
profiles:
|
| 45 |
+
- batch
|
| 46 |
+
volumes:
|
| 47 |
+
- ./output:/app/output
|
| 48 |
+
- ./models:/app/models
|
| 49 |
+
- ./data:/app/data
|
| 50 |
+
environment:
|
| 51 |
+
# Same environment variables as main service
|
| 52 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 53 |
+
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
| 54 |
+
- KOKORO_MODEL_PATH=models/kokoro-v0_19.onnx
|
| 55 |
+
- KOKORO_VOICES_PATH=models/voices.bin
|
| 56 |
+
- KOKORO_DEFAULT_VOICE=af
|
| 57 |
+
- KOKORO_DEFAULT_SPEED=1.0
|
| 58 |
+
- KOKORO_DEFAULT_LANG=en-us
|
| 59 |
+
- PYTHONPATH=/app:$PYTHONPATH
|
| 60 |
+
command: >
|
| 61 |
+
uv run python generate_video.py
|
| 62 |
+
--model "openai/gpt-4o-mini"
|
| 63 |
+
--helper_model "openai/gpt-4o-mini"
|
| 64 |
+
--output_dir "output/batch_generation"
|
| 65 |
+
--theorems_path "data/thb_easy/math.json"
|
| 66 |
+
--max_scene_concurrency 3
|
| 67 |
+
--max_topic_concurrency 5
|
| 68 |
+
restart: no
|
docker-compose.yml
CHANGED
|
@@ -1,80 +0,0 @@
|
|
| 1 |
-
version: '3.8'
|
| 2 |
-
|
| 3 |
-
services:
|
| 4 |
-
theoremexplain:
|
| 5 |
-
build:
|
| 6 |
-
context: .
|
| 7 |
-
dockerfile: dockerfile
|
| 8 |
-
container_name: theoremexplain-agent
|
| 9 |
-
ports:
|
| 10 |
-
- "7860:7860"
|
| 11 |
-
volumes:
|
| 12 |
-
# Mount output directory to persist generated videos
|
| 13 |
-
- ./output:/app/output
|
| 14 |
-
# Mount models directory if you want to use local models
|
| 15 |
-
- ./models:/app/models
|
| 16 |
-
# Mount data directory for RAG and datasets
|
| 17 |
-
- ./data:/app/data
|
| 18 |
-
environment:
|
| 19 |
-
# Copy environment variables from host .env file
|
| 20 |
-
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 21 |
-
- AZURE_API_KEY=${AZURE_API_KEY}
|
| 22 |
-
- AZURE_API_BASE=${AZURE_API_BASE}
|
| 23 |
-
- AZURE_API_VERSION=${AZURE_API_VERSION}
|
| 24 |
-
- VERTEXAI_PROJECT=${VERTEXAI_PROJECT}
|
| 25 |
-
- VERTEXAI_LOCATION=${VERTEXAI_LOCATION}
|
| 26 |
-
- GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS}
|
| 27 |
-
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
| 28 |
-
# Kokoro TTS settings
|
| 29 |
-
- KOKORO_MODEL_PATH=models/kokoro-v0_19.onnx
|
| 30 |
-
- KOKORO_VOICES_PATH=models/voices.bin
|
| 31 |
-
- KOKORO_DEFAULT_VOICE=af
|
| 32 |
-
- KOKORO_DEFAULT_SPEED=1.0
|
| 33 |
-
- KOKORO_DEFAULT_LANG=en-us
|
| 34 |
-
# Python path
|
| 35 |
-
- PYTHONPATH=/app:$PYTHONPATH
|
| 36 |
-
restart: unless-stopped
|
| 37 |
-
healthcheck:
|
| 38 |
-
test: ["CMD", "conda", "run", "-n", "tea", "python", "-c", "import src; import manim; print('Health check passed')"]
|
| 39 |
-
interval: 30s
|
| 40 |
-
timeout: 10s
|
| 41 |
-
retries: 3
|
| 42 |
-
start_period: 60s
|
| 43 |
-
|
| 44 |
-
# Optional: Add a service for running batch generation
|
| 45 |
-
theoremexplain-batch:
|
| 46 |
-
build:
|
| 47 |
-
context: .
|
| 48 |
-
dockerfile: dockerfile
|
| 49 |
-
container_name: theoremexplain-batch
|
| 50 |
-
profiles:
|
| 51 |
-
- batch
|
| 52 |
-
volumes:
|
| 53 |
-
- ./output:/app/output
|
| 54 |
-
- ./models:/app/models
|
| 55 |
-
- ./data:/app/data
|
| 56 |
-
environment:
|
| 57 |
-
# Same environment variables as main service
|
| 58 |
-
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 59 |
-
- AZURE_API_KEY=${AZURE_API_KEY}
|
| 60 |
-
- AZURE_API_BASE=${AZURE_API_BASE}
|
| 61 |
-
- AZURE_API_VERSION=${AZURE_API_VERSION}
|
| 62 |
-
- VERTEXAI_PROJECT=${VERTEXAI_PROJECT}
|
| 63 |
-
- VERTEXAI_LOCATION=${VERTEXAI_LOCATION}
|
| 64 |
-
- GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS}
|
| 65 |
-
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
| 66 |
-
- KOKORO_MODEL_PATH=models/kokoro-v0_19.onnx
|
| 67 |
-
- KOKORO_VOICES_PATH=models/voices.bin
|
| 68 |
-
- KOKORO_DEFAULT_VOICE=af
|
| 69 |
-
- KOKORO_DEFAULT_SPEED=1.0
|
| 70 |
-
- KOKORO_DEFAULT_LANG=en-us
|
| 71 |
-
- PYTHONPATH=/app:$PYTHONPATH
|
| 72 |
-
command: >
|
| 73 |
-
conda run --no-capture-output -n tea python generate_video.py
|
| 74 |
-
--model "openai/gpt-4o-mini"
|
| 75 |
-
--helper_model "openai/gpt-4o-mini"
|
| 76 |
-
--output_dir "output/batch_generation"
|
| 77 |
-
--theorems_path "data/thb_easy/math.json"
|
| 78 |
-
--max_scene_concurrency 3
|
| 79 |
-
--max_topic_concurrency 5
|
| 80 |
-
restart: no
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/client-generation.md
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Client SDK Generation Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to generate and use client SDKs for the Video Generation API.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The Video Generation API supports automatic client SDK generation in multiple programming languages using OpenAPI Generator. This allows developers to quickly integrate with the API using idiomatic code in their preferred language.
|
| 8 |
+
|
| 9 |
+
## Supported Languages
|
| 10 |
+
|
| 11 |
+
- **TypeScript/JavaScript** - Modern TypeScript client with fetch API
|
| 12 |
+
- **Python** - Python client with requests library
|
| 13 |
+
- **Java** - Java client with OkHttp and Gson
|
| 14 |
+
- **C#** - .NET Standard 2.0 compatible client
|
| 15 |
+
- **Go** - Go client with native HTTP library
|
| 16 |
+
- **PHP** - PHP client with Guzzle HTTP
|
| 17 |
+
- **Ruby** - Ruby client with Faraday HTTP
|
| 18 |
+
|
| 19 |
+
## Prerequisites
|
| 20 |
+
|
| 21 |
+
### Required Tools
|
| 22 |
+
|
| 23 |
+
1. **Node.js and npm** - For OpenAPI Generator CLI
|
| 24 |
+
2. **OpenAPI Generator CLI** - For generating clients
|
| 25 |
+
3. **Language-specific tools** (optional, for testing):
|
| 26 |
+
- TypeScript: Node.js, npm
|
| 27 |
+
- Python: Python 3.7+, pip
|
| 28 |
+
- Java: JDK 8+, Maven
|
| 29 |
+
- C#: .NET Core SDK
|
| 30 |
+
- Go: Go 1.16+
|
| 31 |
+
- PHP: PHP 7.4+, Composer
|
| 32 |
+
- Ruby: Ruby 2.7+, Bundler
|
| 33 |
+
|
| 34 |
+
### Installation
|
| 35 |
+
|
| 36 |
+
Install OpenAPI Generator CLI:
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
npm install -g @openapitools/openapi-generator-cli
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## Quick Start
|
| 43 |
+
|
| 44 |
+
### Using the Python Script
|
| 45 |
+
|
| 46 |
+
The easiest way to generate clients is using the provided Python script:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
# Generate all supported clients
|
| 50 |
+
python scripts/generate_clients.py
|
| 51 |
+
|
| 52 |
+
# Generate specific languages
|
| 53 |
+
python scripts/generate_clients.py --languages typescript python
|
| 54 |
+
|
| 55 |
+
# Use custom API URL
|
| 56 |
+
python scripts/generate_clients.py --api-url https://api.example.com
|
| 57 |
+
|
| 58 |
+
# Custom output directory
|
| 59 |
+
python scripts/generate_clients.py --output-dir my_clients
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### Using the Shell Script
|
| 63 |
+
|
| 64 |
+
Alternatively, use the shell script:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
# Generate all clients
|
| 68 |
+
./scripts/generate_clients.sh
|
| 69 |
+
|
| 70 |
+
# Generate specific languages
|
| 71 |
+
./scripts/generate_clients.sh typescript python java
|
| 72 |
+
|
| 73 |
+
# Use custom API URL
|
| 74 |
+
./scripts/generate_clients.sh --api-url https://api.example.com typescript
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
### Manual Generation
|
| 78 |
+
|
| 79 |
+
For manual control, use OpenAPI Generator CLI directly:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
# Fetch OpenAPI specification
|
| 83 |
+
curl -o openapi.json http://localhost:8000/openapi.json
|
| 84 |
+
|
| 85 |
+
# Generate TypeScript client
|
| 86 |
+
openapi-generator-cli generate \
|
| 87 |
+
-i openapi.json \
|
| 88 |
+
-g typescript-fetch \
|
| 89 |
+
-o clients/typescript \
|
| 90 |
+
--package-name video-api-client
|
| 91 |
+
|
| 92 |
+
# Generate Python client
|
| 93 |
+
openapi-generator-cli generate \
|
| 94 |
+
-i openapi.json \
|
| 95 |
+
-g python \
|
| 96 |
+
-o clients/python \
|
| 97 |
+
--package-name video_api_client
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## Generated Client Structure
|
| 101 |
+
|
| 102 |
+
Each generated client includes:
|
| 103 |
+
|
| 104 |
+
```
|
| 105 |
+
clients/
|
| 106 |
+
├── typescript/
|
| 107 |
+
│ ├── src/
|
| 108 |
+
│ │ ├── apis/
|
| 109 |
+
│ │ ├── models/
|
| 110 |
+
│ │ └── index.ts
|
| 111 |
+
│ ├── package.json
|
| 112 |
+
│ ├── README.md
|
| 113 |
+
│ └── example.ts
|
| 114 |
+
├── python/
|
| 115 |
+
│ ├── video_api_client/
|
| 116 |
+
│ │ ├── api/
|
| 117 |
+
│ │ ├── models/
|
| 118 |
+
│ │ └── __init__.py
|
| 119 |
+
│ ├── setup.py
|
| 120 |
+
│ ├── README.md
|
| 121 |
+
│ └── example.py
|
| 122 |
+
└── ...
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## Usage Examples
|
| 126 |
+
|
| 127 |
+
### TypeScript
|
| 128 |
+
|
| 129 |
+
```typescript
|
| 130 |
+
import { Configuration, VideosApi } from 'video-api-client';
|
| 131 |
+
|
| 132 |
+
const config = new Configuration({
|
| 133 |
+
basePath: 'https://api.example.com',
|
| 134 |
+
accessToken: 'your-clerk-session-token'
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
const videosApi = new VideosApi(config);
|
| 138 |
+
|
| 139 |
+
async function generateVideo() {
|
| 140 |
+
try {
|
| 141 |
+
const jobResponse = await videosApi.createVideoGenerationJob({
|
| 142 |
+
configuration: {
|
| 143 |
+
topic: "Pythagorean Theorem",
|
| 144 |
+
context: "Explain the mathematical proof",
|
| 145 |
+
quality: "medium",
|
| 146 |
+
use_rag: true
|
| 147 |
+
}
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
console.log('Job created:', jobResponse.job_id);
|
| 151 |
+
|
| 152 |
+
// Poll for completion
|
| 153 |
+
let status = 'queued';
|
| 154 |
+
while (status !== 'completed' && status !== 'failed') {
|
| 155 |
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
| 156 |
+
|
| 157 |
+
const statusResponse = await videosApi.getVideoJobStatus(jobResponse.job_id);
|
| 158 |
+
status = statusResponse.status;
|
| 159 |
+
|
| 160 |
+
console.log(`Status: ${status} (${statusResponse.progress.percentage}%)`);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
if (status === 'completed') {
|
| 164 |
+
const videoBlob = await videosApi.downloadVideoFile(jobResponse.job_id);
|
| 165 |
+
console.log('Video downloaded');
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
} catch (error) {
|
| 169 |
+
console.error('Error:', error);
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### Python
|
| 175 |
+
|
| 176 |
+
```python
|
| 177 |
+
import time
|
| 178 |
+
from video_api_client import Configuration, ApiClient, VideosApi
|
| 179 |
+
from video_api_client.models import JobCreateRequest, JobConfiguration
|
| 180 |
+
|
| 181 |
+
configuration = Configuration(
|
| 182 |
+
host="https://api.example.com",
|
| 183 |
+
access_token="your-clerk-session-token"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
with ApiClient(configuration) as api_client:
|
| 187 |
+
videos_api = VideosApi(api_client)
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
# Create job
|
| 191 |
+
job_request = JobCreateRequest(
|
| 192 |
+
configuration=JobConfiguration(
|
| 193 |
+
topic="Pythagorean Theorem",
|
| 194 |
+
context="Explain the mathematical proof",
|
| 195 |
+
quality="medium",
|
| 196 |
+
use_rag=True
|
| 197 |
+
)
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
job_response = videos_api.create_video_generation_job(job_request)
|
| 201 |
+
print(f"Job created: {job_response.job_id}")
|
| 202 |
+
|
| 203 |
+
# Poll for completion
|
| 204 |
+
status = "queued"
|
| 205 |
+
while status not in ["completed", "failed"]:
|
| 206 |
+
time.sleep(5)
|
| 207 |
+
|
| 208 |
+
status_response = videos_api.get_video_job_status(job_response.job_id)
|
| 209 |
+
status = status_response.status
|
| 210 |
+
|
| 211 |
+
print(f"Status: {status} ({status_response.progress.percentage}%)")
|
| 212 |
+
|
| 213 |
+
if status == "completed":
|
| 214 |
+
video_data = videos_api.download_video_file(job_response.job_id)
|
| 215 |
+
with open("video.mp4", "wb") as f:
|
| 216 |
+
f.write(video_data)
|
| 217 |
+
print("Video downloaded")
|
| 218 |
+
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f"Error: {e}")
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
### Java
|
| 224 |
+
|
| 225 |
+
```java
|
| 226 |
+
import com.example.videoapiclient.ApiClient;
|
| 227 |
+
import com.example.videoapiclient.Configuration;
|
| 228 |
+
import com.example.videoapiclient.api.VideosApi;
|
| 229 |
+
import com.example.videoapiclient.model.*;
|
| 230 |
+
|
| 231 |
+
public class VideoApiExample {
|
| 232 |
+
public static void main(String[] args) {
|
| 233 |
+
ApiClient client = Configuration.getDefaultApiClient();
|
| 234 |
+
client.setBasePath("https://api.example.com");
|
| 235 |
+
client.setAccessToken("your-clerk-session-token");
|
| 236 |
+
|
| 237 |
+
VideosApi videosApi = new VideosApi(client);
|
| 238 |
+
|
| 239 |
+
try {
|
| 240 |
+
JobConfiguration config = new JobConfiguration()
|
| 241 |
+
.topic("Pythagorean Theorem")
|
| 242 |
+
.context("Explain the mathematical proof")
|
| 243 |
+
.quality(VideoQuality.MEDIUM)
|
| 244 |
+
.useRag(true);
|
| 245 |
+
|
| 246 |
+
JobCreateRequest request = new JobCreateRequest().configuration(config);
|
| 247 |
+
JobResponse jobResponse = videosApi.createVideoGenerationJob(request);
|
| 248 |
+
|
| 249 |
+
System.out.println("Job created: " + jobResponse.getJobId());
|
| 250 |
+
|
| 251 |
+
// Poll for completion
|
| 252 |
+
String status = "queued";
|
| 253 |
+
while (!"completed".equals(status) && !"failed".equals(status)) {
|
| 254 |
+
Thread.sleep(5000);
|
| 255 |
+
|
| 256 |
+
JobStatusResponse statusResponse = videosApi.getVideoJobStatus(jobResponse.getJobId());
|
| 257 |
+
status = statusResponse.getStatus().getValue();
|
| 258 |
+
|
| 259 |
+
System.out.println("Status: " + status + " (" +
|
| 260 |
+
statusResponse.getProgress().getPercentage() + "%)");
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
if ("completed".equals(status)) {
|
| 264 |
+
System.out.println("Video generation completed!");
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
} catch (Exception e) {
|
| 268 |
+
System.err.println("Error: " + e.getMessage());
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
## Authentication
|
| 275 |
+
|
| 276 |
+
All clients support Clerk authentication. Set your session token when configuring the client:
|
| 277 |
+
|
| 278 |
+
### TypeScript
|
| 279 |
+
```typescript
|
| 280 |
+
const config = new Configuration({
|
| 281 |
+
accessToken: 'your-clerk-session-token'
|
| 282 |
+
});
|
| 283 |
+
```
|
| 284 |
+
|
| 285 |
+
### Python
|
| 286 |
+
```python
|
| 287 |
+
configuration.access_token = 'your-clerk-session-token'
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
### Java
|
| 291 |
+
```java
|
| 292 |
+
client.setAccessToken("your-clerk-session-token");
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
## Error Handling
|
| 296 |
+
|
| 297 |
+
All clients provide structured error handling:
|
| 298 |
+
|
| 299 |
+
### TypeScript
|
| 300 |
+
```typescript
|
| 301 |
+
try {
|
| 302 |
+
const response = await api.createVideoGenerationJob(request);
|
| 303 |
+
} catch (error) {
|
| 304 |
+
if (error.status === 422) {
|
| 305 |
+
console.error('Validation error:', error.body);
|
| 306 |
+
} else if (error.status === 429) {
|
| 307 |
+
console.error('Rate limit exceeded');
|
| 308 |
+
} else {
|
| 309 |
+
console.error('API error:', error);
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
```
|
| 313 |
+
|
| 314 |
+
### Python
|
| 315 |
+
```python
|
| 316 |
+
from video_api_client.exceptions import ApiException
|
| 317 |
+
|
| 318 |
+
try:
|
| 319 |
+
response = api.create_video_generation_job(request)
|
| 320 |
+
except ApiException as e:
|
| 321 |
+
if e.status == 422:
|
| 322 |
+
print(f"Validation error: {e.body}")
|
| 323 |
+
elif e.status == 429:
|
| 324 |
+
print("Rate limit exceeded")
|
| 325 |
+
else:
|
| 326 |
+
print(f"API error: {e}")
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
## Testing Generated Clients
|
| 330 |
+
|
| 331 |
+
Use the provided testing script to validate generated clients:
|
| 332 |
+
|
| 333 |
+
```bash
|
| 334 |
+
# Test all generated clients
|
| 335 |
+
python scripts/test_clients.py
|
| 336 |
+
|
| 337 |
+
# Test with custom API URL
|
| 338 |
+
python scripts/test_clients.py --api-url https://api.example.com
|
| 339 |
+
|
| 340 |
+
# Save test results
|
| 341 |
+
python scripts/test_clients.py --output-file test_results.json
|
| 342 |
+
|
| 343 |
+
# Verbose output
|
| 344 |
+
python scripts/test_clients.py --verbose
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
The test script validates:
|
| 348 |
+
- Client structure and files
|
| 349 |
+
- Build/compilation success
|
| 350 |
+
- Import/usage capability
|
| 351 |
+
- API connectivity
|
| 352 |
+
- Basic functionality
|
| 353 |
+
|
| 354 |
+
## Customization
|
| 355 |
+
|
| 356 |
+
### Custom Templates
|
| 357 |
+
|
| 358 |
+
Create custom templates in the `templates/` directory:
|
| 359 |
+
|
| 360 |
+
```
|
| 361 |
+
templates/
|
| 362 |
+
├── typescript/
|
| 363 |
+
│ ├── api.mustache
|
| 364 |
+
│ ├── model.mustache
|
| 365 |
+
│ └── README.mustache
|
| 366 |
+
├── python/
|
| 367 |
+
│ ├── api.mustache
|
| 368 |
+
│ └── model.mustache
|
| 369 |
+
└── ...
|
| 370 |
+
```
|
| 371 |
+
|
| 372 |
+
### Configuration File
|
| 373 |
+
|
| 374 |
+
Modify `openapi-generator-config.yaml` to customize generation:
|
| 375 |
+
|
| 376 |
+
```yaml
|
| 377 |
+
typescript:
|
| 378 |
+
generatorName: typescript-fetch
|
| 379 |
+
additionalProperties:
|
| 380 |
+
npmName: my-custom-client
|
| 381 |
+
supportsES6: true
|
| 382 |
+
withInterfaces: true
|
| 383 |
+
```
|
| 384 |
+
|
| 385 |
+
### Custom Properties
|
| 386 |
+
|
| 387 |
+
Pass additional properties during generation:
|
| 388 |
+
|
| 389 |
+
```bash
|
| 390 |
+
openapi-generator-cli generate \
|
| 391 |
+
-i openapi.json \
|
| 392 |
+
-g typescript-fetch \
|
| 393 |
+
-o clients/typescript \
|
| 394 |
+
--additional-properties=npmName=my-client,supportsES6=true
|
| 395 |
+
```
|
| 396 |
+
|
| 397 |
+
## Publishing Clients
|
| 398 |
+
|
| 399 |
+
### NPM (TypeScript/JavaScript)
|
| 400 |
+
|
| 401 |
+
```bash
|
| 402 |
+
cd clients/typescript
|
| 403 |
+
npm publish
|
| 404 |
+
```
|
| 405 |
+
|
| 406 |
+
### PyPI (Python)
|
| 407 |
+
|
| 408 |
+
```bash
|
| 409 |
+
cd clients/python
|
| 410 |
+
python setup.py sdist bdist_wheel
|
| 411 |
+
twine upload dist/*
|
| 412 |
+
```
|
| 413 |
+
|
| 414 |
+
### Maven Central (Java)
|
| 415 |
+
|
| 416 |
+
```bash
|
| 417 |
+
cd clients/java
|
| 418 |
+
mvn deploy
|
| 419 |
+
```
|
| 420 |
+
|
| 421 |
+
### NuGet (C#)
|
| 422 |
+
|
| 423 |
+
```bash
|
| 424 |
+
cd clients/csharp
|
| 425 |
+
dotnet pack
|
| 426 |
+
dotnet nuget push *.nupkg
|
| 427 |
+
```
|
| 428 |
+
|
| 429 |
+
## Troubleshooting
|
| 430 |
+
|
| 431 |
+
### Common Issues
|
| 432 |
+
|
| 433 |
+
1. **OpenAPI Generator not found**
|
| 434 |
+
```bash
|
| 435 |
+
npm install -g @openapitools/openapi-generator-cli
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
2. **API server not running**
|
| 439 |
+
```bash
|
| 440 |
+
# Start the API server first
|
| 441 |
+
python -m uvicorn src.app.main:app --reload
|
| 442 |
+
```
|
| 443 |
+
|
| 444 |
+
3. **Build failures**
|
| 445 |
+
- Check language-specific requirements
|
| 446 |
+
- Verify generated code syntax
|
| 447 |
+
- Review error messages in build logs
|
| 448 |
+
|
| 449 |
+
4. **Import errors**
|
| 450 |
+
- Ensure proper installation
|
| 451 |
+
- Check Python path and virtual environments
|
| 452 |
+
- Verify package structure
|
| 453 |
+
|
| 454 |
+
### Debug Mode
|
| 455 |
+
|
| 456 |
+
Enable debug output for troubleshooting:
|
| 457 |
+
|
| 458 |
+
```bash
|
| 459 |
+
# Python script
|
| 460 |
+
python scripts/generate_clients.py --verbose
|
| 461 |
+
|
| 462 |
+
# OpenAPI Generator
|
| 463 |
+
openapi-generator-cli generate \
|
| 464 |
+
--verbose \
|
| 465 |
+
--debug-operations \
|
| 466 |
+
-i openapi.json \
|
| 467 |
+
-g typescript-fetch \
|
| 468 |
+
-o clients/typescript
|
| 469 |
+
```
|
| 470 |
+
|
| 471 |
+
### Validation
|
| 472 |
+
|
| 473 |
+
Validate OpenAPI specification before generation:
|
| 474 |
+
|
| 475 |
+
```bash
|
| 476 |
+
# Using OpenAPI Generator
|
| 477 |
+
openapi-generator-cli validate -i openapi.json
|
| 478 |
+
|
| 479 |
+
# Using Swagger Editor online
|
| 480 |
+
# Visit: https://editor.swagger.io/
|
| 481 |
+
```
|
| 482 |
+
|
| 483 |
+
## Best Practices
|
| 484 |
+
|
| 485 |
+
1. **Version Management**
|
| 486 |
+
- Tag client versions with API versions
|
| 487 |
+
- Maintain backward compatibility
|
| 488 |
+
- Document breaking changes
|
| 489 |
+
|
| 490 |
+
2. **Testing**
|
| 491 |
+
- Test clients against real API
|
| 492 |
+
- Include integration tests
|
| 493 |
+
- Validate error handling
|
| 494 |
+
|
| 495 |
+
3. **Documentation**
|
| 496 |
+
- Include usage examples
|
| 497 |
+
- Document authentication setup
|
| 498 |
+
- Provide troubleshooting guides
|
| 499 |
+
|
| 500 |
+
4. **Distribution**
|
| 501 |
+
- Use semantic versioning
|
| 502 |
+
- Publish to appropriate package managers
|
| 503 |
+
- Maintain changelogs
|
| 504 |
+
|
| 505 |
+
## Support
|
| 506 |
+
|
| 507 |
+
For issues with client generation:
|
| 508 |
+
|
| 509 |
+
- Check the [OpenAPI Generator documentation](https://openapi-generator.tech/)
|
| 510 |
+
- Review API documentation at `/docs`
|
| 511 |
+
- Contact support at [email protected]
|
| 512 |
+
- File issues on GitHub
|
| 513 |
+
|
| 514 |
+
## Contributing
|
| 515 |
+
|
| 516 |
+
To contribute to client generation:
|
| 517 |
+
|
| 518 |
+
1. Fork the repository
|
| 519 |
+
2. Create custom templates or improve scripts
|
| 520 |
+
3. Test with multiple languages
|
| 521 |
+
4. Submit pull request with documentation
|
| 522 |
+
|
| 523 |
+
## License
|
| 524 |
+
|
| 525 |
+
Generated clients inherit the same license as the API project (MIT License).
|
extract_token.html
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Clerk Token Extractor</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: Arial, sans-serif;
|
| 10 |
+
max-width: 800px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background-color: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 30px;
|
| 18 |
+
border-radius: 10px;
|
| 19 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 20 |
+
}
|
| 21 |
+
h1 {
|
| 22 |
+
color: #333;
|
| 23 |
+
text-align: center;
|
| 24 |
+
}
|
| 25 |
+
.step {
|
| 26 |
+
margin: 20px 0;
|
| 27 |
+
padding: 15px;
|
| 28 |
+
background: #f8f9fa;
|
| 29 |
+
border-left: 4px solid #007bff;
|
| 30 |
+
border-radius: 5px;
|
| 31 |
+
}
|
| 32 |
+
.step h3 {
|
| 33 |
+
margin-top: 0;
|
| 34 |
+
color: #007bff;
|
| 35 |
+
}
|
| 36 |
+
button {
|
| 37 |
+
background: #007bff;
|
| 38 |
+
color: white;
|
| 39 |
+
border: none;
|
| 40 |
+
padding: 10px 20px;
|
| 41 |
+
border-radius: 5px;
|
| 42 |
+
cursor: pointer;
|
| 43 |
+
font-size: 16px;
|
| 44 |
+
margin: 10px 5px;
|
| 45 |
+
}
|
| 46 |
+
button:hover {
|
| 47 |
+
background: #0056b3;
|
| 48 |
+
}
|
| 49 |
+
.token-display {
|
| 50 |
+
background: #e9ecef;
|
| 51 |
+
padding: 15px;
|
| 52 |
+
border-radius: 5px;
|
| 53 |
+
font-family: monospace;
|
| 54 |
+
word-break: break-all;
|
| 55 |
+
margin: 10px 0;
|
| 56 |
+
min-height: 50px;
|
| 57 |
+
}
|
| 58 |
+
.success {
|
| 59 |
+
color: #28a745;
|
| 60 |
+
font-weight: bold;
|
| 61 |
+
}
|
| 62 |
+
.error {
|
| 63 |
+
color: #dc3545;
|
| 64 |
+
font-weight: bold;
|
| 65 |
+
}
|
| 66 |
+
.info {
|
| 67 |
+
background: #d1ecf1;
|
| 68 |
+
border: 1px solid #bee5eb;
|
| 69 |
+
color: #0c5460;
|
| 70 |
+
padding: 10px;
|
| 71 |
+
border-radius: 5px;
|
| 72 |
+
margin: 10px 0;
|
| 73 |
+
}
|
| 74 |
+
</style>
|
| 75 |
+
</head>
|
| 76 |
+
<body>
|
| 77 |
+
<div class="container">
|
| 78 |
+
<h1>🎫 Clerk Token Extractor</h1>
|
| 79 |
+
<p>This tool helps you extract your Clerk authentication token for API testing.</p>
|
| 80 |
+
|
| 81 |
+
<div class="info">
|
| 82 |
+
<strong>Prerequisites:</strong> You must be signed in to your application in this browser session.
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
<div class="step">
|
| 86 |
+
<h3>Step 1: Check if Clerk is Available</h3>
|
| 87 |
+
<button onclick="checkClerk()">Check Clerk Status</button>
|
| 88 |
+
<div id="clerkStatus"></div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div class="step">
|
| 92 |
+
<h3>Step 2: Extract Token</h3>
|
| 93 |
+
<button onclick="extractToken()">Get Current Token</button>
|
| 94 |
+
<button onclick="extractTokenAsync()">Get Fresh Token</button>
|
| 95 |
+
<div id="tokenResult"></div>
|
| 96 |
+
<div id="tokenDisplay" class="token-display"></div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div class="step">
|
| 100 |
+
<h3>Step 3: Copy Token</h3>
|
| 101 |
+
<button onclick="copyToken()" id="copyBtn" disabled>Copy Token to Clipboard</button>
|
| 102 |
+
<button onclick="downloadToken()" id="downloadBtn" disabled>Download as File</button>
|
| 103 |
+
<div id="copyResult"></div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div class="step">
|
| 107 |
+
<h3>Step 4: Test Your Token</h3>
|
| 108 |
+
<p>Use your token with the test scripts:</p>
|
| 109 |
+
<code>python test_video_simple.py https://your-api-url.com/api/v1 YOUR_TOKEN_HERE</code>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div class="step">
|
| 113 |
+
<h3>Alternative Methods</h3>
|
| 114 |
+
<details>
|
| 115 |
+
<summary>Manual Browser Console Method</summary>
|
| 116 |
+
<ol>
|
| 117 |
+
<li>Open Developer Tools (F12)</li>
|
| 118 |
+
<li>Go to Console tab</li>
|
| 119 |
+
<li>Run: <code>window.Clerk.session.getToken().then(token => console.log(token))</code></li>
|
| 120 |
+
<li>Copy the token from console output</li>
|
| 121 |
+
</ol>
|
| 122 |
+
</details>
|
| 123 |
+
|
| 124 |
+
<details>
|
| 125 |
+
<summary>Browser Storage Method</summary>
|
| 126 |
+
<ol>
|
| 127 |
+
<li>Open Developer Tools (F12)</li>
|
| 128 |
+
<li>Go to Application/Storage tab</li>
|
| 129 |
+
<li>Look in Local Storage for keys starting with '__clerk_'</li>
|
| 130 |
+
<li>Find the session data and extract the JWT token</li>
|
| 131 |
+
</ol>
|
| 132 |
+
</details>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<script>
|
| 137 |
+
let currentToken = null;
|
| 138 |
+
|
| 139 |
+
function checkClerk() {
|
| 140 |
+
const statusDiv = document.getElementById('clerkStatus');
|
| 141 |
+
|
| 142 |
+
if (typeof window.Clerk === 'undefined') {
|
| 143 |
+
statusDiv.innerHTML = '<span class="error">❌ Clerk not found. Make sure you\'re on a page with Clerk loaded.</span>';
|
| 144 |
+
return false;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
if (!window.Clerk.session) {
|
| 148 |
+
statusDiv.innerHTML = '<span class="error">❌ No active Clerk session. Please sign in first.</span>';
|
| 149 |
+
return false;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
statusDiv.innerHTML = '<span class="success">✅ Clerk is available and you have an active session!</span>';
|
| 153 |
+
return true;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function extractToken() {
|
| 157 |
+
if (!checkClerk()) return;
|
| 158 |
+
|
| 159 |
+
const resultDiv = document.getElementById('tokenResult');
|
| 160 |
+
const tokenDiv = document.getElementById('tokenDisplay');
|
| 161 |
+
|
| 162 |
+
try {
|
| 163 |
+
// Try to get token synchronously first
|
| 164 |
+
const session = window.Clerk.session;
|
| 165 |
+
if (session && session.getToken) {
|
| 166 |
+
// This might be async, so we'll handle both cases
|
| 167 |
+
const tokenResult = session.getToken();
|
| 168 |
+
|
| 169 |
+
if (tokenResult && typeof tokenResult.then === 'function') {
|
| 170 |
+
// It's a promise
|
| 171 |
+
resultDiv.innerHTML = '<span class="info">Getting token asynchronously...</span>';
|
| 172 |
+
tokenResult.then(token => {
|
| 173 |
+
if (token) {
|
| 174 |
+
currentToken = token;
|
| 175 |
+
tokenDiv.textContent = token;
|
| 176 |
+
resultDiv.innerHTML = '<span class="success">✅ Token extracted successfully!</span>';
|
| 177 |
+
enableButtons();
|
| 178 |
+
} else {
|
| 179 |
+
resultDiv.innerHTML = '<span class="error">❌ Token is null or empty</span>';
|
| 180 |
+
}
|
| 181 |
+
}).catch(error => {
|
| 182 |
+
resultDiv.innerHTML = `<span class="error">❌ Error getting token: ${error.message}</span>`;
|
| 183 |
+
});
|
| 184 |
+
} else if (tokenResult) {
|
| 185 |
+
// It's synchronous
|
| 186 |
+
currentToken = tokenResult;
|
| 187 |
+
tokenDiv.textContent = tokenResult;
|
| 188 |
+
resultDiv.innerHTML = '<span class="success">✅ Token extracted successfully!</span>';
|
| 189 |
+
enableButtons();
|
| 190 |
+
} else {
|
| 191 |
+
resultDiv.innerHTML = '<span class="error">❌ Could not get token</span>';
|
| 192 |
+
}
|
| 193 |
+
} else {
|
| 194 |
+
resultDiv.innerHTML = '<span class="error">❌ Session.getToken method not available</span>';
|
| 195 |
+
}
|
| 196 |
+
} catch (error) {
|
| 197 |
+
resultDiv.innerHTML = `<span class="error">❌ Error: ${error.message}</span>`;
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
function extractTokenAsync() {
|
| 202 |
+
if (!checkClerk()) return;
|
| 203 |
+
|
| 204 |
+
const resultDiv = document.getElementById('tokenResult');
|
| 205 |
+
const tokenDiv = document.getElementById('tokenDisplay');
|
| 206 |
+
|
| 207 |
+
try {
|
| 208 |
+
resultDiv.innerHTML = '<span class="info">Getting fresh token...</span>';
|
| 209 |
+
|
| 210 |
+
window.Clerk.session.getToken()
|
| 211 |
+
.then(token => {
|
| 212 |
+
if (token) {
|
| 213 |
+
currentToken = token;
|
| 214 |
+
tokenDiv.textContent = token;
|
| 215 |
+
resultDiv.innerHTML = '<span class="success">✅ Fresh token extracted successfully!</span>';
|
| 216 |
+
enableButtons();
|
| 217 |
+
} else {
|
| 218 |
+
resultDiv.innerHTML = '<span class="error">❌ Token is null or empty</span>';
|
| 219 |
+
}
|
| 220 |
+
})
|
| 221 |
+
.catch(error => {
|
| 222 |
+
resultDiv.innerHTML = `<span class="error">❌ Error getting token: ${error.message}</span>`;
|
| 223 |
+
});
|
| 224 |
+
} catch (error) {
|
| 225 |
+
resultDiv.innerHTML = `<span class="error">❌ Error: ${error.message}</span>`;
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
function enableButtons() {
|
| 230 |
+
document.getElementById('copyBtn').disabled = false;
|
| 231 |
+
document.getElementById('downloadBtn').disabled = false;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
function copyToken() {
|
| 235 |
+
if (!currentToken) {
|
| 236 |
+
document.getElementById('copyResult').innerHTML = '<span class="error">❌ No token to copy</span>';
|
| 237 |
+
return;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
navigator.clipboard.writeText(currentToken).then(() => {
|
| 241 |
+
document.getElementById('copyResult').innerHTML = '<span class="success">✅ Token copied to clipboard!</span>';
|
| 242 |
+
}).catch(err => {
|
| 243 |
+
// Fallback for older browsers
|
| 244 |
+
const textArea = document.createElement('textarea');
|
| 245 |
+
textArea.value = currentToken;
|
| 246 |
+
document.body.appendChild(textArea);
|
| 247 |
+
textArea.select();
|
| 248 |
+
document.execCommand('copy');
|
| 249 |
+
document.body.removeChild(textArea);
|
| 250 |
+
document.getElementById('copyResult').innerHTML = '<span class="success">✅ Token copied to clipboard!</span>';
|
| 251 |
+
});
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function downloadToken() {
|
| 255 |
+
if (!currentToken) {
|
| 256 |
+
document.getElementById('copyResult').innerHTML = '<span class="error">❌ No token to download</span>';
|
| 257 |
+
return;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
const blob = new Blob([currentToken], { type: 'text/plain' });
|
| 261 |
+
const url = window.URL.createObjectURL(blob);
|
| 262 |
+
const a = document.createElement('a');
|
| 263 |
+
a.href = url;
|
| 264 |
+
a.download = 'clerk_token.txt';
|
| 265 |
+
document.body.appendChild(a);
|
| 266 |
+
a.click();
|
| 267 |
+
document.body.removeChild(a);
|
| 268 |
+
window.URL.revokeObjectURL(url);
|
| 269 |
+
|
| 270 |
+
document.getElementById('copyResult').innerHTML = '<span class="success">✅ Token downloaded as file!</span>';
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// Auto-check Clerk status on page load
|
| 274 |
+
window.addEventListener('load', () => {
|
| 275 |
+
setTimeout(checkClerk, 1000);
|
| 276 |
+
});
|
| 277 |
+
</script>
|
| 278 |
+
</body>
|
| 279 |
+
</html>
|
fastapi-backend-pyproject.toml
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["hatchling"]
|
| 3 |
+
build-backend = "hatchling.build"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "fastapi-video-backend"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "FastAPI backend for multi-agent video generation system"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.11"
|
| 11 |
+
license = {text = "MIT"}
|
| 12 |
+
authors = [
|
| 13 |
+
{name = "Video Generation Team"},
|
| 14 |
+
]
|
| 15 |
+
keywords = ["fastapi", "video", "generation", "api"]
|
| 16 |
+
classifiers = [
|
| 17 |
+
"Development Status :: 3 - Alpha",
|
| 18 |
+
"Intended Audience :: Developers",
|
| 19 |
+
"License :: OSI Approved :: MIT License",
|
| 20 |
+
"Programming Language :: Python :: 3",
|
| 21 |
+
"Programming Language :: Python :: 3.11",
|
| 22 |
+
"Programming Language :: Python :: 3.12",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
dependencies = [
|
| 26 |
+
# FastAPI and ASGI server
|
| 27 |
+
"fastapi>=0.104.0",
|
| 28 |
+
"uvicorn[standard]>=0.24.0",
|
| 29 |
+
|
| 30 |
+
# Pydantic for data validation
|
| 31 |
+
"pydantic>=2.5.0",
|
| 32 |
+
"pydantic-settings>=2.1.0",
|
| 33 |
+
|
| 34 |
+
# Redis for caching and job queuing
|
| 35 |
+
"redis>=5.0.0",
|
| 36 |
+
"hiredis>=2.2.0", # C extension for better Redis performance
|
| 37 |
+
|
| 38 |
+
# Clerk SDK for authentication
|
| 39 |
+
"clerk-backend-api>=1.0.0",
|
| 40 |
+
|
| 41 |
+
# HTTP client for external API calls
|
| 42 |
+
"httpx>=0.25.0",
|
| 43 |
+
|
| 44 |
+
# JSON Web Token handling
|
| 45 |
+
"pyjwt[crypto]>=2.8.0",
|
| 46 |
+
|
| 47 |
+
# File handling and validation
|
| 48 |
+
"python-multipart>=0.0.6", # For file uploads
|
| 49 |
+
"python-magic>=0.4.27", # File type detection
|
| 50 |
+
|
| 51 |
+
# Logging and monitoring
|
| 52 |
+
"structlog>=23.2.0",
|
| 53 |
+
|
| 54 |
+
# Environment variable management
|
| 55 |
+
"python-dotenv>=1.0.0",
|
| 56 |
+
|
| 57 |
+
# Date and time utilities
|
| 58 |
+
"python-dateutil>=2.8.2",
|
| 59 |
+
|
| 60 |
+
# Async utilities
|
| 61 |
+
"asyncio-mqtt>=0.16.0", # If needed for real-time updates
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
[project.optional-dependencies]
|
| 65 |
+
dev = [
|
| 66 |
+
# Testing
|
| 67 |
+
"pytest>=7.4.0",
|
| 68 |
+
"pytest-asyncio>=0.21.0",
|
| 69 |
+
"pytest-cov>=4.1.0",
|
| 70 |
+
"httpx>=0.25.0", # For testing FastAPI endpoints
|
| 71 |
+
|
| 72 |
+
# Code quality
|
| 73 |
+
"black>=23.0.0",
|
| 74 |
+
"isort>=5.12.0",
|
| 75 |
+
"flake8>=6.0.0",
|
| 76 |
+
"mypy>=1.7.0",
|
| 77 |
+
|
| 78 |
+
# Development tools
|
| 79 |
+
"pre-commit>=3.5.0",
|
| 80 |
+
"watchfiles>=0.21.0", # For auto-reload during development
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
production = [
|
| 84 |
+
# Production ASGI server
|
| 85 |
+
"gunicorn>=21.2.0",
|
| 86 |
+
|
| 87 |
+
# Monitoring and observability
|
| 88 |
+
"prometheus-client>=0.19.0",
|
| 89 |
+
"opentelemetry-api>=1.21.0",
|
| 90 |
+
"opentelemetry-sdk>=1.21.0",
|
| 91 |
+
"opentelemetry-instrumentation-fastapi>=0.42b0",
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
[project.urls]
|
| 95 |
+
Homepage = "https://github.com/your-org/fastapi-video-backend"
|
| 96 |
+
Documentation = "https://your-org.github.io/fastapi-video-backend"
|
| 97 |
+
Repository = "https://github.com/your-org/fastapi-video-backend.git"
|
| 98 |
+
Issues = "https://github.com/your-org/fastapi-video-backend/issues"
|
| 99 |
+
|
| 100 |
+
[tool.hatch.build.targets.wheel]
|
| 101 |
+
packages = ["src/app"]
|
| 102 |
+
|
| 103 |
+
[tool.black]
|
| 104 |
+
line-length = 88
|
| 105 |
+
target-version = ['py311']
|
| 106 |
+
include = '\.pyi?$'
|
| 107 |
+
extend-exclude = '''
|
| 108 |
+
/(
|
| 109 |
+
# directories
|
| 110 |
+
\.eggs
|
| 111 |
+
| \.git
|
| 112 |
+
| \.hg
|
| 113 |
+
| \.mypy_cache
|
| 114 |
+
| \.tox
|
| 115 |
+
| \.venv
|
| 116 |
+
| build
|
| 117 |
+
| dist
|
| 118 |
+
)/
|
| 119 |
+
'''
|
| 120 |
+
|
| 121 |
+
[tool.isort]
|
| 122 |
+
profile = "black"
|
| 123 |
+
multi_line_output = 3
|
| 124 |
+
line_length = 88
|
| 125 |
+
known_first_party = ["app"]
|
| 126 |
+
|
| 127 |
+
[tool.mypy]
|
| 128 |
+
python_version = "3.11"
|
| 129 |
+
warn_return_any = true
|
| 130 |
+
warn_unused_configs = true
|
| 131 |
+
disallow_untyped_defs = true
|
| 132 |
+
disallow_incomplete_defs = true
|
| 133 |
+
check_untyped_defs = true
|
| 134 |
+
disallow_untyped_decorators = true
|
| 135 |
+
no_implicit_optional = true
|
| 136 |
+
warn_redundant_casts = true
|
| 137 |
+
warn_unused_ignores = true
|
| 138 |
+
warn_no_return = true
|
| 139 |
+
warn_unreachable = true
|
| 140 |
+
strict_equality = true
|
| 141 |
+
|
| 142 |
+
[[tool.mypy.overrides]]
|
| 143 |
+
module = [
|
| 144 |
+
"redis.*",
|
| 145 |
+
"clerk_backend_api.*",
|
| 146 |
+
"structlog.*",
|
| 147 |
+
]
|
| 148 |
+
ignore_missing_imports = true
|
| 149 |
+
|
| 150 |
+
[tool.pytest.ini_options]
|
| 151 |
+
minversion = "7.0"
|
| 152 |
+
addopts = "-ra -q --strict-markers --strict-config"
|
| 153 |
+
testpaths = ["tests"]
|
| 154 |
+
python_files = ["test_*.py", "*_test.py"]
|
| 155 |
+
python_classes = ["Test*"]
|
| 156 |
+
python_functions = ["test_*"]
|
| 157 |
+
asyncio_mode = "auto"
|
| 158 |
+
|
| 159 |
+
[tool.coverage.run]
|
| 160 |
+
source = ["src"]
|
| 161 |
+
omit = [
|
| 162 |
+
"*/tests/*",
|
| 163 |
+
"*/test_*",
|
| 164 |
+
"*/__pycache__/*",
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
[tool.coverage.report]
|
| 168 |
+
exclude_lines = [
|
| 169 |
+
"pragma: no cover",
|
| 170 |
+
"def __repr__",
|
| 171 |
+
"if self.debug:",
|
| 172 |
+
"if settings.DEBUG",
|
| 173 |
+
"raise AssertionError",
|
| 174 |
+
"raise NotImplementedError",
|
| 175 |
+
"if 0:",
|
| 176 |
+
"if __name__ == .__main__.:",
|
| 177 |
+
"class .*\\bProtocol\\):",
|
| 178 |
+
"@(abc\\.)?abstractmethod",
|
| 179 |
+
]
|
main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from t2m!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
ngrok-dev.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "2"
|
| 2 |
+
authtoken: ${NGROK_AUTHTOKEN}
|
| 3 |
+
|
| 4 |
+
tunnels:
|
| 5 |
+
fastapi-dev:
|
| 6 |
+
addr: host.docker.internal:8000 # Points to localhost:8000 on host
|
| 7 |
+
proto: http
|
| 8 |
+
schemes: [https, http]
|
| 9 |
+
inspect: true
|
| 10 |
+
bind_tls: true
|
| 11 |
+
|
| 12 |
+
web_addr: 0.0.0.0:4040
|
ngrok.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "2"
|
| 2 |
+
authtoken: ${NGROK_AUTHTOKEN}
|
| 3 |
+
|
| 4 |
+
tunnels:
|
| 5 |
+
fastapi:
|
| 6 |
+
addr: fastapi:8000
|
| 7 |
+
proto: http
|
| 8 |
+
schemes: [https, http]
|
| 9 |
+
inspect: true
|
| 10 |
+
bind_tls: true
|
| 11 |
+
|
| 12 |
+
web_addr: 0.0.0.0:4040
|
openapi-generator-config.yaml
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenAPI Generator Configuration for Video Generation API
|
| 2 |
+
# This file contains global configuration for generating client SDKs
|
| 3 |
+
|
| 4 |
+
# Global properties
|
| 5 |
+
globalProperties:
|
| 6 |
+
models: true
|
| 7 |
+
apis: true
|
| 8 |
+
supportingFiles: true
|
| 9 |
+
modelTests: true
|
| 10 |
+
apiTests: true
|
| 11 |
+
modelDocs: true
|
| 12 |
+
apiDocs: true
|
| 13 |
+
|
| 14 |
+
# TypeScript configuration
|
| 15 |
+
typescript:
|
| 16 |
+
generatorName: typescript-fetch
|
| 17 |
+
outputDir: clients/typescript
|
| 18 |
+
additionalProperties:
|
| 19 |
+
npmName: video-api-client
|
| 20 |
+
npmVersion: 1.0.0
|
| 21 |
+
supportsES6: true
|
| 22 |
+
withInterfaces: true
|
| 23 |
+
typescriptThreePlus: true
|
| 24 |
+
enumPropertyNaming: PascalCase
|
| 25 |
+
modelPropertyNaming: camelCase
|
| 26 |
+
paramNaming: camelCase
|
| 27 |
+
stringEnums: true
|
| 28 |
+
withSeparateModelsAndApi: true
|
| 29 |
+
apiPackage: api
|
| 30 |
+
modelPackage: models
|
| 31 |
+
templateDir: templates/typescript
|
| 32 |
+
|
| 33 |
+
# Python configuration
|
| 34 |
+
python:
|
| 35 |
+
generatorName: python
|
| 36 |
+
outputDir: clients/python
|
| 37 |
+
additionalProperties:
|
| 38 |
+
packageName: video_api_client
|
| 39 |
+
projectName: video-api-client
|
| 40 |
+
packageVersion: 1.0.0
|
| 41 |
+
packageUrl: https://github.com/example/video-api-client-python
|
| 42 |
+
packageDescription: Python client library for Video Generation API
|
| 43 |
+
authorName: API Team
|
| 44 |
+
authorEmail: [email protected]
|
| 45 |
+
library: urllib3
|
| 46 |
+
generateSourceCodeOnly: false
|
| 47 |
+
templateDir: templates/python
|
| 48 |
+
|
| 49 |
+
# Java configuration
|
| 50 |
+
java:
|
| 51 |
+
generatorName: java
|
| 52 |
+
outputDir: clients/java
|
| 53 |
+
additionalProperties:
|
| 54 |
+
groupId: com.example
|
| 55 |
+
artifactId: video-api-client
|
| 56 |
+
artifactVersion: 1.0.0
|
| 57 |
+
artifactDescription: Java client library for Video Generation API
|
| 58 |
+
library: okhttp-gson
|
| 59 |
+
java8: true
|
| 60 |
+
dateLibrary: java8
|
| 61 |
+
serializationLibrary: gson
|
| 62 |
+
hideGenerationTimestamp: true
|
| 63 |
+
templateDir: templates/java
|
| 64 |
+
|
| 65 |
+
# C# configuration
|
| 66 |
+
csharp:
|
| 67 |
+
generatorName: csharp
|
| 68 |
+
outputDir: clients/csharp
|
| 69 |
+
additionalProperties:
|
| 70 |
+
packageName: VideoApiClient
|
| 71 |
+
packageVersion: 1.0.0
|
| 72 |
+
clientPackage: VideoApiClient
|
| 73 |
+
packageCompany: Example Inc
|
| 74 |
+
packageAuthors: API Team
|
| 75 |
+
packageCopyright: Copyright 2024
|
| 76 |
+
packageDescription: C# client library for Video Generation API
|
| 77 |
+
targetFramework: netstandard2.0
|
| 78 |
+
library: httpclient
|
| 79 |
+
generatePropertyChanged: true
|
| 80 |
+
templateDir: templates/csharp
|
| 81 |
+
|
| 82 |
+
# Go configuration
|
| 83 |
+
go:
|
| 84 |
+
generatorName: go
|
| 85 |
+
outputDir: clients/go
|
| 86 |
+
additionalProperties:
|
| 87 |
+
packageName: videoapiclient
|
| 88 |
+
packageVersion: 1.0.0
|
| 89 |
+
packageUrl: github.com/example/video-api-client-go
|
| 90 |
+
hideGenerationTimestamp: true
|
| 91 |
+
withGoCodegenComment: true
|
| 92 |
+
enumClassPrefix: true
|
| 93 |
+
templateDir: templates/go
|
| 94 |
+
|
| 95 |
+
# PHP configuration
|
| 96 |
+
php:
|
| 97 |
+
generatorName: php
|
| 98 |
+
outputDir: clients/php
|
| 99 |
+
additionalProperties:
|
| 100 |
+
packageName: VideoApiClient
|
| 101 |
+
composerVendorName: example
|
| 102 |
+
composerProjectName: video-api-client
|
| 103 |
+
packageVersion: 1.0.0
|
| 104 |
+
invokerPackage: VideoApiClient
|
| 105 |
+
srcBasePath: src
|
| 106 |
+
hideGenerationTimestamp: true
|
| 107 |
+
templateDir: templates/php
|
| 108 |
+
|
| 109 |
+
# Ruby configuration
|
| 110 |
+
ruby:
|
| 111 |
+
generatorName: ruby
|
| 112 |
+
outputDir: clients/ruby
|
| 113 |
+
additionalProperties:
|
| 114 |
+
gemName: video_api_client
|
| 115 |
+
gemVersion: 1.0.0
|
| 116 |
+
gemHomepage: https://github.com/example/video-api-client-ruby
|
| 117 |
+
gemSummary: Ruby client library for Video Generation API
|
| 118 |
+
gemDescription: Ruby client library for the Video Generation API
|
| 119 |
+
gemAuthor: API Team
|
| 120 |
+
gemAuthorEmail: [email protected]
|
| 121 |
+
moduleName: VideoApiClient
|
| 122 |
+
hideGenerationTimestamp: true
|
| 123 |
+
templateDir: templates/ruby
|
| 124 |
+
|
| 125 |
+
# Custom templates directory structure
|
| 126 |
+
templates:
|
| 127 |
+
typescript:
|
| 128 |
+
- api.mustache
|
| 129 |
+
- model.mustache
|
| 130 |
+
- README.mustache
|
| 131 |
+
- package.mustache
|
| 132 |
+
python:
|
| 133 |
+
- api.mustache
|
| 134 |
+
- model.mustache
|
| 135 |
+
- README.mustache
|
| 136 |
+
- setup.mustache
|
| 137 |
+
java:
|
| 138 |
+
- api.mustache
|
| 139 |
+
- model.mustache
|
| 140 |
+
- README.mustache
|
| 141 |
+
- pom.mustache
|
| 142 |
+
csharp:
|
| 143 |
+
- api.mustache
|
| 144 |
+
- model.mustache
|
| 145 |
+
- README.mustache
|
| 146 |
+
- Project.mustache
|
| 147 |
+
go:
|
| 148 |
+
- api.mustache
|
| 149 |
+
- model.mustache
|
| 150 |
+
- README.mustache
|
| 151 |
+
- go.mustache
|
| 152 |
+
php:
|
| 153 |
+
- api.mustache
|
| 154 |
+
- model.mustache
|
| 155 |
+
- README.mustache
|
| 156 |
+
- composer.mustache
|
| 157 |
+
ruby:
|
| 158 |
+
- api.mustache
|
| 159 |
+
- model.mustache
|
| 160 |
+
- README.mustache
|
| 161 |
+
- gemspec.mustache
|
| 162 |
+
|
| 163 |
+
# Documentation configuration
|
| 164 |
+
documentation:
|
| 165 |
+
generateApiDocs: true
|
| 166 |
+
generateModelDocs: true
|
| 167 |
+
generateExamples: true
|
| 168 |
+
includeAuthentication: true
|
| 169 |
+
includeErrorHandling: true
|
| 170 |
+
includeRateLimiting: true
|
| 171 |
+
|
| 172 |
+
# Validation rules
|
| 173 |
+
validation:
|
| 174 |
+
validateSpec: true
|
| 175 |
+
strictMode: false
|
| 176 |
+
allowAdditionalPropertiesWithComposedSchema: true
|
| 177 |
+
|
| 178 |
+
# Post-processing options
|
| 179 |
+
postProcessing:
|
| 180 |
+
removeUnusedImports: true
|
| 181 |
+
formatCode: true
|
| 182 |
+
generateTests: true
|
| 183 |
+
generateExamples: true
|
pyproject.toml
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "t2m"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"annotated-types~=0.7.0",
|
| 9 |
+
"asyncio-mqtt>=0.16.0",
|
| 10 |
+
"azure-cognitiveservices-speech~=1.41.1",
|
| 11 |
+
"boto3~=1.36.9",
|
| 12 |
+
"cachetools~=5.5.0",
|
| 13 |
+
"cairosvg>=2.8.2",
|
| 14 |
+
"certifi~=2024.8.30",
|
| 15 |
+
"charset-normalizer~=3.4.0",
|
| 16 |
+
"chromadb~=0.6.3",
|
| 17 |
+
"clerk-backend-api>=1.0.0",
|
| 18 |
+
"click~=8.1.7",
|
| 19 |
+
"cloup~=3.0.5",
|
| 20 |
+
"cython~=3.0.11",
|
| 21 |
+
"decorator~=5.1.1",
|
| 22 |
+
"fastapi>=0.116.1",
|
| 23 |
+
"ffmpeg-python~=0.2.0",
|
| 24 |
+
"glcontext~=3.0.0",
|
| 25 |
+
"google-ai-generativelanguage~=0.6.10",
|
| 26 |
+
"google-api-core~=2.22.0",
|
| 27 |
+
"google-api-python-client~=2.151.0",
|
| 28 |
+
"google-auth~=2.35.0",
|
| 29 |
+
"google-auth-httplib2~=0.2.0",
|
| 30 |
+
"google-cloud-aiplatform~=1.79.0",
|
| 31 |
+
"google-generativeai~=0.8.3",
|
| 32 |
+
"googleapis-common-protos~=1.65.0",
|
| 33 |
+
"gradio>=5.43.1",
|
| 34 |
+
"grpcio~=1.67.1",
|
| 35 |
+
"grpcio-status~=1.67.1",
|
| 36 |
+
"gtts~=2.5.3",
|
| 37 |
+
"hiredis>=2.2.0",
|
| 38 |
+
"httplib2~=0.22.0",
|
| 39 |
+
"httpx>=0.25.0",
|
| 40 |
+
"idna~=3.10",
|
| 41 |
+
"imageio-ffmpeg~=0.5.1",
|
| 42 |
+
"inquirer>=3.4.1",
|
| 43 |
+
"isosurfaces~=0.1.2",
|
| 44 |
+
"kokoro-onnx>=0.4.9",
|
| 45 |
+
"krippendorff~=0.8.1",
|
| 46 |
+
"langchain~=0.3.14",
|
| 47 |
+
"langchain-community~=0.3.14",
|
| 48 |
+
"langfuse~=2.58.1",
|
| 49 |
+
"litellm~=1.60.5",
|
| 50 |
+
"manim~=0.18.1",
|
| 51 |
+
"manim-chemistry~=0.4.4",
|
| 52 |
+
"manim-circuit~=0.0.3",
|
| 53 |
+
"manim-dsa~=0.2.0",
|
| 54 |
+
"manim-ml~=0.0.24",
|
| 55 |
+
"manim-physics~=0.4.0",
|
| 56 |
+
"manim-voiceover~=0.3.7",
|
| 57 |
+
"manimpango~=0.6.0",
|
| 58 |
+
"mapbox-earcut~=1.0.2",
|
| 59 |
+
"markdown-it-py~=3.0.0",
|
| 60 |
+
"mdurl~=0.1.2",
|
| 61 |
+
"moderngl~=5.12.0",
|
| 62 |
+
"moviepy~=2.1.2",
|
| 63 |
+
"multipledispatch~=1.0.0",
|
| 64 |
+
"mutagen~=1.47.0",
|
| 65 |
+
"networkx~=3.4.2",
|
| 66 |
+
"numpy~=2.2.2",
|
| 67 |
+
"openai~=1.61.0",
|
| 68 |
+
"opencv-python~=4.11.0",
|
| 69 |
+
"pillow>=10.4.0",
|
| 70 |
+
"proto-plus~=1.25.0",
|
| 71 |
+
"protobuf~=5.28.3",
|
| 72 |
+
"psutil>=7.0.0",
|
| 73 |
+
"pyasn1~=0.6.1",
|
| 74 |
+
"pyasn1-modules~=0.4.1",
|
| 75 |
+
"pyaudio~=0.2.14",
|
| 76 |
+
"pycairo~=1.27.0",
|
| 77 |
+
"pydantic>=2.5.0",
|
| 78 |
+
"pydantic-core~=2.23.4",
|
| 79 |
+
"pydantic-settings>=2.1.0",
|
| 80 |
+
"pydub~=0.25.1",
|
| 81 |
+
"pyglet~=2.0.18",
|
| 82 |
+
"pygments~=2.18.0",
|
| 83 |
+
"pyjwt[crypto]>=2.8.0",
|
| 84 |
+
"pylatexenc~=2.10",
|
| 85 |
+
"pyparsing~=3.2.0",
|
| 86 |
+
"pyrr~=0.10.3",
|
| 87 |
+
"pysrt>=1.1.2",
|
| 88 |
+
"pytest>=8.4.1",
|
| 89 |
+
"pytest-asyncio>=1.1.0",
|
| 90 |
+
"python-dateutil>=2.8.2",
|
| 91 |
+
"python-dotenv~=0.21.1",
|
| 92 |
+
"python-magic>=0.4.27",
|
| 93 |
+
"python-multipart>=0.0.6",
|
| 94 |
+
"python-slugify~=8.0.4",
|
| 95 |
+
"redis>=5.0.0",
|
| 96 |
+
"requests~=2.32.3",
|
| 97 |
+
"rich~=13.9.3",
|
| 98 |
+
"rsa~=4.9",
|
| 99 |
+
"scipy~=1.14.1",
|
| 100 |
+
"screeninfo~=0.8.1",
|
| 101 |
+
"sentence-transformers>=5.1.0",
|
| 102 |
+
"sentencepiece>=0.2.1",
|
| 103 |
+
"skia-pathops~=0.8.0.post2",
|
| 104 |
+
"soundfile~=0.13.1",
|
| 105 |
+
"sox~=1.5.0",
|
| 106 |
+
"speechrecognition~=3.14.1",
|
| 107 |
+
"srt~=3.5.3",
|
| 108 |
+
"statsmodels~=0.14.4",
|
| 109 |
+
"structlog>=23.2.0",
|
| 110 |
+
"svgelements~=1.9.6",
|
| 111 |
+
"text-unidecode~=1.3",
|
| 112 |
+
"tiktoken~=0.8.0",
|
| 113 |
+
"timm>=1.0.19",
|
| 114 |
+
"tqdm~=4.66.5",
|
| 115 |
+
"transformers>=4.55.4",
|
| 116 |
+
"typing-extensions~=4.12.2",
|
| 117 |
+
"uritemplate~=4.1.1",
|
| 118 |
+
"urllib3~=2.2.3",
|
| 119 |
+
"uvicorn[standard]>=0.24.0",
|
| 120 |
+
"watchdog~=5.0.3",
|
| 121 |
+
"yt-dlp>=2025.8.22",
|
| 122 |
+
]
|
quick_api_test.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Quick API Test - Just check if your API is running
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import requests
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
def quick_test(base_url):
|
| 10 |
+
"""Quick test to see if API is responding"""
|
| 11 |
+
print(f"🔍 Quick API Test: {base_url}")
|
| 12 |
+
print("-" * 40)
|
| 13 |
+
|
| 14 |
+
# Test endpoints that should work
|
| 15 |
+
endpoints = [
|
| 16 |
+
('/system/health', 'System Health'),
|
| 17 |
+
('/auth/health', 'Auth Health'),
|
| 18 |
+
('/auth/status', 'Auth Status'),
|
| 19 |
+
('/', 'Root (may require auth)')
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
working_endpoints = 0
|
| 23 |
+
|
| 24 |
+
for endpoint, name in endpoints:
|
| 25 |
+
try:
|
| 26 |
+
url = f"{base_url}{endpoint}"
|
| 27 |
+
response = requests.get(url, timeout=10)
|
| 28 |
+
|
| 29 |
+
if response.status_code == 200:
|
| 30 |
+
print(f"✅ {name}: Working (200)")
|
| 31 |
+
working_endpoints += 1
|
| 32 |
+
elif response.status_code == 401:
|
| 33 |
+
print(f"🔐 {name}: Requires auth (401)")
|
| 34 |
+
working_endpoints += 1 # Still counts as working
|
| 35 |
+
else:
|
| 36 |
+
print(f"⚠️ {name}: Status {response.status_code}")
|
| 37 |
+
|
| 38 |
+
except requests.exceptions.ConnectionError:
|
| 39 |
+
print(f"❌ {name}: Connection failed - API not running?")
|
| 40 |
+
except requests.exceptions.Timeout:
|
| 41 |
+
print(f"❌ {name}: Timeout")
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"❌ {name}: Error - {e}")
|
| 44 |
+
|
| 45 |
+
print(f"\n📊 Result: {working_endpoints}/{len(endpoints)} endpoints responding")
|
| 46 |
+
|
| 47 |
+
if working_endpoints >= 2:
|
| 48 |
+
print("✅ API appears to be running!")
|
| 49 |
+
print("\nNext steps:")
|
| 50 |
+
print("1. Get your Clerk token: python get_token_simple.py")
|
| 51 |
+
print("2. Test with token: python test_current_api.py <base_url> <token>")
|
| 52 |
+
return True
|
| 53 |
+
else:
|
| 54 |
+
print("❌ API may not be running or accessible")
|
| 55 |
+
print("\nTroubleshooting:")
|
| 56 |
+
print("- Check if your FastAPI server is running")
|
| 57 |
+
print("- Verify the URL is correct")
|
| 58 |
+
print("- Check for firewall/network issues")
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
def main():
|
| 62 |
+
if len(sys.argv) != 2:
|
| 63 |
+
print("Usage: python quick_api_test.py <base_url>")
|
| 64 |
+
print("Example: python quick_api_test.py http://localhost:8000/api/v1")
|
| 65 |
+
return 1
|
| 66 |
+
|
| 67 |
+
base_url = sys.argv[1].rstrip('/')
|
| 68 |
+
success = quick_test(base_url)
|
| 69 |
+
return 0 if success else 1
|
| 70 |
+
|
| 71 |
+
if __name__ == '__main__':
|
| 72 |
+
exit(main())
|
requirements-test.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements for API testing
|
| 2 |
+
requests>=2.31.0
|
| 3 |
+
urllib3>=2.0.0
|
requirements.txt
CHANGED
|
@@ -97,4 +97,38 @@ soundfile~=0.13.1
|
|
| 97 |
krippendorff~=0.8.1
|
| 98 |
statsmodels~=0.14.4
|
| 99 |
opencv-python~=4.11.0
|
| 100 |
-
gradio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
krippendorff~=0.8.1
|
| 98 |
statsmodels~=0.14.4
|
| 99 |
opencv-python~=4.11.0
|
| 100 |
+
gradio
|
| 101 |
+
# FastAPI and ASGI server
|
| 102 |
+
fastapi
|
| 103 |
+
uvicorn[standard]>=0.24.0
|
| 104 |
+
|
| 105 |
+
# Pydantic for data validation
|
| 106 |
+
pydantic>=2.5.0
|
| 107 |
+
pydantic-settings>=2.1.0
|
| 108 |
+
|
| 109 |
+
# Redis for caching and job queuing
|
| 110 |
+
redis>=5.0.0
|
| 111 |
+
hiredis>=2.2.0 # C extension for better Redis performanc
|
| 112 |
+
|
| 113 |
+
# Clerk SDK for authentication
|
| 114 |
+
clerk-backend-api>=1.0.0
|
| 115 |
+
|
| 116 |
+
# HTTP client for external API calls
|
| 117 |
+
httpx>=0.25.0
|
| 118 |
+
|
| 119 |
+
# JSON Web Token handling
|
| 120 |
+
pyjwt[crypto]>=2.8.0
|
| 121 |
+
|
| 122 |
+
# File handling and validation
|
| 123 |
+
python-multipart>=0.0.6 # For file uploads
|
| 124 |
+
python-magic>=0.4.27 # File type detection
|
| 125 |
+
|
| 126 |
+
# Logging and monitoring
|
| 127 |
+
structlog>=23.2.0
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# Date and time utilities
|
| 131 |
+
python-dateutil>=2.8.2
|
| 132 |
+
|
| 133 |
+
# Async utilities
|
| 134 |
+
asyncio-mqtt>=0.16.0 # If needed for real-time updates
|
run_api_tests.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple runner for T2M API tests with configuration file support
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from test_api_endpoints import T2MAPITester
|
| 11 |
+
|
| 12 |
+
def load_config(config_file: str = "test_config.json") -> dict:
|
| 13 |
+
"""Load configuration from JSON file"""
|
| 14 |
+
try:
|
| 15 |
+
with open(config_file, 'r') as f:
|
| 16 |
+
return json.load(f)
|
| 17 |
+
except FileNotFoundError:
|
| 18 |
+
print(f"❌ Configuration file '{config_file}' not found")
|
| 19 |
+
print("Please create a test_config.json file or use test_api_endpoints.py directly")
|
| 20 |
+
return None
|
| 21 |
+
except json.JSONDecodeError as e:
|
| 22 |
+
print(f"❌ Invalid JSON in configuration file: {e}")
|
| 23 |
+
return None
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
# Load configuration
|
| 27 |
+
config = load_config()
|
| 28 |
+
if not config:
|
| 29 |
+
return 1
|
| 30 |
+
|
| 31 |
+
api_config = config.get('api_config', {})
|
| 32 |
+
base_url = api_config.get('base_url')
|
| 33 |
+
token = api_config.get('token')
|
| 34 |
+
|
| 35 |
+
if not base_url:
|
| 36 |
+
print("❌ base_url not specified in configuration")
|
| 37 |
+
return 1
|
| 38 |
+
|
| 39 |
+
if not token or token == "your-bearer-token-here":
|
| 40 |
+
print("⚠️ No valid token provided - only public endpoints will be tested")
|
| 41 |
+
token = None
|
| 42 |
+
|
| 43 |
+
print("🔧 Configuration loaded:")
|
| 44 |
+
print(f" Base URL: {base_url}")
|
| 45 |
+
print(f" Token: {'Provided' if token else 'Not provided'}")
|
| 46 |
+
|
| 47 |
+
# Create and run tester
|
| 48 |
+
tester = T2MAPITester(base_url, token)
|
| 49 |
+
tester.run_all_tests()
|
| 50 |
+
|
| 51 |
+
return 0
|
| 52 |
+
|
| 53 |
+
if __name__ == '__main__':
|
| 54 |
+
exit(main())
|
run_debug.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Run debug and comprehensive tests
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
def run_debug():
|
| 10 |
+
print("🔍 Running Authentication Debug")
|
| 11 |
+
print("=" * 50)
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
result = subprocess.run([sys.executable, "debug_auth.py"],
|
| 15 |
+
capture_output=True, text=True, timeout=30)
|
| 16 |
+
print(result.stdout)
|
| 17 |
+
if result.stderr:
|
| 18 |
+
print("STDERR:", result.stderr)
|
| 19 |
+
|
| 20 |
+
if result.returncode != 0:
|
| 21 |
+
print(f"Debug script failed with code {result.returncode}")
|
| 22 |
+
|
| 23 |
+
except subprocess.TimeoutExpired:
|
| 24 |
+
print("Debug script timed out")
|
| 25 |
+
except Exception as e:
|
| 26 |
+
print(f"Error running debug script: {e}")
|
| 27 |
+
|
| 28 |
+
print("\n" + "=" * 50)
|
| 29 |
+
print("🎬 Running Comprehensive Test")
|
| 30 |
+
print("=" * 50)
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
result = subprocess.run([sys.executable, "test_api_comprehensive.py"],
|
| 34 |
+
capture_output=True, text=True, timeout=60)
|
| 35 |
+
print(result.stdout)
|
| 36 |
+
if result.stderr:
|
| 37 |
+
print("STDERR:", result.stderr)
|
| 38 |
+
|
| 39 |
+
if result.returncode == 0:
|
| 40 |
+
print("✅ All tests passed!")
|
| 41 |
+
else:
|
| 42 |
+
print(f"❌ Some tests failed (code {result.returncode})")
|
| 43 |
+
|
| 44 |
+
except subprocess.TimeoutExpired:
|
| 45 |
+
print("Test script timed out")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"Error running test script: {e}")
|
| 48 |
+
|
| 49 |
+
if __name__ == '__main__':
|
| 50 |
+
run_debug()
|
run_video_test.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple runner for video generation API tests
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
from test_video_generation import VideoGenerationTester
|
| 10 |
+
|
| 11 |
+
def load_config(config_file: str = "video_test_config.json") -> dict:
|
| 12 |
+
"""Load configuration from JSON file"""
|
| 13 |
+
try:
|
| 14 |
+
with open(config_file, 'r') as f:
|
| 15 |
+
return json.load(f)
|
| 16 |
+
except FileNotFoundError:
|
| 17 |
+
print(f"❌ Configuration file '{config_file}' not found")
|
| 18 |
+
print("Please create a video_test_config.json file or use test_video_generation.py directly")
|
| 19 |
+
return None
|
| 20 |
+
except json.JSONDecodeError as e:
|
| 21 |
+
print(f"❌ Invalid JSON in configuration file: {e}")
|
| 22 |
+
return None
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
# Load configuration
|
| 26 |
+
config = load_config()
|
| 27 |
+
if not config:
|
| 28 |
+
return 1
|
| 29 |
+
|
| 30 |
+
api_config = config.get('api', {})
|
| 31 |
+
base_url = api_config.get('base_url')
|
| 32 |
+
token = api_config.get('token')
|
| 33 |
+
|
| 34 |
+
if not base_url:
|
| 35 |
+
print("❌ base_url not specified in configuration")
|
| 36 |
+
return 1
|
| 37 |
+
|
| 38 |
+
if not token or token == "your-bearer-token-here":
|
| 39 |
+
print("❌ Valid authentication token is required for video generation testing")
|
| 40 |
+
return 1
|
| 41 |
+
|
| 42 |
+
print("🎬 Video Generation Test Configuration:")
|
| 43 |
+
print(f" Base URL: {base_url}")
|
| 44 |
+
print(f" Token: {'*' * (len(token) - 4) + token[-4:]}")
|
| 45 |
+
|
| 46 |
+
# Get test settings
|
| 47 |
+
test_settings = config.get('test_settings', {})
|
| 48 |
+
monitor_progress = test_settings.get('monitor_progress', True)
|
| 49 |
+
|
| 50 |
+
# Create and run tester
|
| 51 |
+
tester = VideoGenerationTester(base_url, token)
|
| 52 |
+
tester.run_comprehensive_test(monitor_progress)
|
| 53 |
+
|
| 54 |
+
return 0
|
| 55 |
+
|
| 56 |
+
if __name__ == '__main__':
|
| 57 |
+
exit(main())
|
scripts/generate_clients.py
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Client SDK generation script for FastAPI Video Generation Backend.
|
| 4 |
+
|
| 5 |
+
This script generates client SDKs in multiple languages using OpenAPI Generator.
|
| 6 |
+
It fetches the OpenAPI specification from the running API and generates clients
|
| 7 |
+
for TypeScript, Python, Java, C#, Go, PHP, and Ruby.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import json
|
| 13 |
+
import subprocess
|
| 14 |
+
import argparse
|
| 15 |
+
import requests
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import Dict, List, Optional
|
| 18 |
+
import tempfile
|
| 19 |
+
import shutil
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ClientGenerator:
|
| 23 |
+
"""Client SDK generator using OpenAPI Generator."""
|
| 24 |
+
|
| 25 |
+
SUPPORTED_LANGUAGES = {
|
| 26 |
+
"typescript": {
|
| 27 |
+
"generator": "typescript-fetch",
|
| 28 |
+
"output_dir": "clients/typescript",
|
| 29 |
+
"package_name": "video-api-client",
|
| 30 |
+
"additional_properties": {
|
| 31 |
+
"npmName": "video-api-client",
|
| 32 |
+
"npmVersion": "1.0.0",
|
| 33 |
+
"supportsES6": "true",
|
| 34 |
+
"withInterfaces": "true",
|
| 35 |
+
"typescriptThreePlus": "true"
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"python": {
|
| 39 |
+
"generator": "python",
|
| 40 |
+
"output_dir": "clients/python",
|
| 41 |
+
"package_name": "video_api_client",
|
| 42 |
+
"additional_properties": {
|
| 43 |
+
"packageName": "video_api_client",
|
| 44 |
+
"projectName": "video-api-client",
|
| 45 |
+
"packageVersion": "1.0.0",
|
| 46 |
+
"packageUrl": "https://github.com/example/video-api-client-python"
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
"java": {
|
| 50 |
+
"generator": "java",
|
| 51 |
+
"output_dir": "clients/java",
|
| 52 |
+
"package_name": "com.example.videoapiclient",
|
| 53 |
+
"additional_properties": {
|
| 54 |
+
"groupId": "com.example",
|
| 55 |
+
"artifactId": "video-api-client",
|
| 56 |
+
"artifactVersion": "1.0.0",
|
| 57 |
+
"library": "okhttp-gson",
|
| 58 |
+
"java8": "true"
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"csharp": {
|
| 62 |
+
"generator": "csharp",
|
| 63 |
+
"output_dir": "clients/csharp",
|
| 64 |
+
"package_name": "VideoApiClient",
|
| 65 |
+
"additional_properties": {
|
| 66 |
+
"packageName": "VideoApiClient",
|
| 67 |
+
"packageVersion": "1.0.0",
|
| 68 |
+
"clientPackage": "VideoApiClient",
|
| 69 |
+
"packageCompany": "Example Inc",
|
| 70 |
+
"packageAuthors": "API Team",
|
| 71 |
+
"packageCopyright": "Copyright 2024",
|
| 72 |
+
"packageDescription": "Video Generation API Client for .NET",
|
| 73 |
+
"targetFramework": "netstandard2.0"
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"go": {
|
| 77 |
+
"generator": "go",
|
| 78 |
+
"output_dir": "clients/go",
|
| 79 |
+
"package_name": "videoapiclient",
|
| 80 |
+
"additional_properties": {
|
| 81 |
+
"packageName": "videoapiclient",
|
| 82 |
+
"packageVersion": "1.0.0",
|
| 83 |
+
"packageUrl": "github.com/example/video-api-client-go"
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"php": {
|
| 87 |
+
"generator": "php",
|
| 88 |
+
"output_dir": "clients/php",
|
| 89 |
+
"package_name": "VideoApiClient",
|
| 90 |
+
"additional_properties": {
|
| 91 |
+
"packageName": "VideoApiClient",
|
| 92 |
+
"composerVendorName": "example",
|
| 93 |
+
"composerProjectName": "video-api-client",
|
| 94 |
+
"packageVersion": "1.0.0"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"ruby": {
|
| 98 |
+
"generator": "ruby",
|
| 99 |
+
"output_dir": "clients/ruby",
|
| 100 |
+
"package_name": "video_api_client",
|
| 101 |
+
"additional_properties": {
|
| 102 |
+
"gemName": "video_api_client",
|
| 103 |
+
"gemVersion": "1.0.0",
|
| 104 |
+
"gemHomepage": "https://github.com/example/video-api-client-ruby",
|
| 105 |
+
"gemSummary": "Video Generation API Client for Ruby",
|
| 106 |
+
"gemDescription": "Ruby client library for the Video Generation API"
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
def __init__(self, api_url: str = "http://localhost:8000", output_base_dir: str = "generated_clients"):
|
| 112 |
+
self.api_url = api_url.rstrip("/")
|
| 113 |
+
self.output_base_dir = Path(output_base_dir)
|
| 114 |
+
self.openapi_spec_path: Optional[Path] = None
|
| 115 |
+
|
| 116 |
+
def fetch_openapi_spec(self) -> Path:
|
| 117 |
+
"""Fetch OpenAPI specification from the API."""
|
| 118 |
+
try:
|
| 119 |
+
print(f"Fetching OpenAPI specification from {self.api_url}/openapi.json")
|
| 120 |
+
response = requests.get(f"{self.api_url}/openapi.json", timeout=30)
|
| 121 |
+
response.raise_for_status()
|
| 122 |
+
|
| 123 |
+
# Create temporary file for OpenAPI spec
|
| 124 |
+
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
| 125 |
+
json.dump(response.json(), temp_file, indent=2)
|
| 126 |
+
temp_file.close()
|
| 127 |
+
|
| 128 |
+
self.openapi_spec_path = Path(temp_file.name)
|
| 129 |
+
print(f"OpenAPI specification saved to {self.openapi_spec_path}")
|
| 130 |
+
return self.openapi_spec_path
|
| 131 |
+
|
| 132 |
+
except requests.RequestException as e:
|
| 133 |
+
print(f"Error fetching OpenAPI specification: {e}")
|
| 134 |
+
sys.exit(1)
|
| 135 |
+
|
| 136 |
+
def check_openapi_generator(self) -> bool:
|
| 137 |
+
"""Check if OpenAPI Generator is available."""
|
| 138 |
+
try:
|
| 139 |
+
result = subprocess.run(
|
| 140 |
+
["openapi-generator-cli", "version"],
|
| 141 |
+
capture_output=True,
|
| 142 |
+
text=True,
|
| 143 |
+
timeout=10
|
| 144 |
+
)
|
| 145 |
+
if result.returncode == 0:
|
| 146 |
+
print(f"OpenAPI Generator found: {result.stdout.strip()}")
|
| 147 |
+
return True
|
| 148 |
+
else:
|
| 149 |
+
print("OpenAPI Generator not found or not working properly")
|
| 150 |
+
return False
|
| 151 |
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
| 152 |
+
print("OpenAPI Generator CLI not found")
|
| 153 |
+
return False
|
| 154 |
+
|
| 155 |
+
def install_openapi_generator(self) -> bool:
|
| 156 |
+
"""Install OpenAPI Generator CLI using npm."""
|
| 157 |
+
try:
|
| 158 |
+
print("Installing OpenAPI Generator CLI...")
|
| 159 |
+
result = subprocess.run(
|
| 160 |
+
["npm", "install", "-g", "@openapitools/openapi-generator-cli"],
|
| 161 |
+
capture_output=True,
|
| 162 |
+
text=True,
|
| 163 |
+
timeout=120
|
| 164 |
+
)
|
| 165 |
+
if result.returncode == 0:
|
| 166 |
+
print("OpenAPI Generator CLI installed successfully")
|
| 167 |
+
return True
|
| 168 |
+
else:
|
| 169 |
+
print(f"Failed to install OpenAPI Generator CLI: {result.stderr}")
|
| 170 |
+
return False
|
| 171 |
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
| 172 |
+
print("npm not found. Please install Node.js and npm first.")
|
| 173 |
+
return False
|
| 174 |
+
|
| 175 |
+
def generate_client(self, language: str, custom_config: Optional[Dict] = None) -> bool:
|
| 176 |
+
"""Generate client SDK for specified language."""
|
| 177 |
+
if language not in self.SUPPORTED_LANGUAGES:
|
| 178 |
+
print(f"Unsupported language: {language}")
|
| 179 |
+
return False
|
| 180 |
+
|
| 181 |
+
config = self.SUPPORTED_LANGUAGES[language].copy()
|
| 182 |
+
if custom_config:
|
| 183 |
+
config.update(custom_config)
|
| 184 |
+
|
| 185 |
+
output_dir = self.output_base_dir / config["output_dir"]
|
| 186 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 187 |
+
|
| 188 |
+
# Build OpenAPI Generator command
|
| 189 |
+
cmd = [
|
| 190 |
+
"openapi-generator-cli",
|
| 191 |
+
"generate",
|
| 192 |
+
"-i", str(self.openapi_spec_path),
|
| 193 |
+
"-g", config["generator"],
|
| 194 |
+
"-o", str(output_dir),
|
| 195 |
+
"--package-name", config["package_name"]
|
| 196 |
+
]
|
| 197 |
+
|
| 198 |
+
# Add additional properties
|
| 199 |
+
if "additional_properties" in config:
|
| 200 |
+
for key, value in config["additional_properties"].items():
|
| 201 |
+
cmd.extend(["--additional-properties", f"{key}={value}"])
|
| 202 |
+
|
| 203 |
+
# Add global properties for better client generation
|
| 204 |
+
cmd.extend([
|
| 205 |
+
"--global-property",
|
| 206 |
+
"models,apis,supportingFiles,modelTests,apiTests,modelDocs,apiDocs"
|
| 207 |
+
])
|
| 208 |
+
|
| 209 |
+
print(f"Generating {language} client...")
|
| 210 |
+
print(f"Command: {' '.join(cmd)}")
|
| 211 |
+
|
| 212 |
+
try:
|
| 213 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
| 214 |
+
if result.returncode == 0:
|
| 215 |
+
print(f"✅ {language} client generated successfully in {output_dir}")
|
| 216 |
+
self._post_process_client(language, output_dir)
|
| 217 |
+
return True
|
| 218 |
+
else:
|
| 219 |
+
print(f"❌ Failed to generate {language} client:")
|
| 220 |
+
print(result.stderr)
|
| 221 |
+
return False
|
| 222 |
+
except subprocess.TimeoutExpired:
|
| 223 |
+
print(f"❌ Timeout generating {language} client")
|
| 224 |
+
return False
|
| 225 |
+
|
| 226 |
+
def _post_process_client(self, language: str, output_dir: Path) -> None:
|
| 227 |
+
"""Post-process generated client with additional files and documentation."""
|
| 228 |
+
# Create README for the client
|
| 229 |
+
readme_content = self._generate_client_readme(language)
|
| 230 |
+
readme_path = output_dir / "README.md"
|
| 231 |
+
readme_path.write_text(readme_content)
|
| 232 |
+
|
| 233 |
+
# Create example usage file
|
| 234 |
+
example_content = self._generate_client_example(language)
|
| 235 |
+
if example_content:
|
| 236 |
+
example_extensions = {
|
| 237 |
+
"typescript": "ts",
|
| 238 |
+
"python": "py",
|
| 239 |
+
"java": "java",
|
| 240 |
+
"csharp": "cs",
|
| 241 |
+
"go": "go",
|
| 242 |
+
"php": "php",
|
| 243 |
+
"ruby": "rb"
|
| 244 |
+
}
|
| 245 |
+
extension = example_extensions.get(language, "txt")
|
| 246 |
+
example_path = output_dir / f"example.{extension}"
|
| 247 |
+
example_path.write_text(example_content)
|
| 248 |
+
|
| 249 |
+
# Language-specific post-processing
|
| 250 |
+
if language == "typescript":
|
| 251 |
+
self._post_process_typescript(output_dir)
|
| 252 |
+
elif language == "python":
|
| 253 |
+
self._post_process_python(output_dir)
|
| 254 |
+
|
| 255 |
+
def _post_process_typescript(self, output_dir: Path) -> None:
|
| 256 |
+
"""Post-process TypeScript client."""
|
| 257 |
+
# Create package.json if it doesn't exist
|
| 258 |
+
package_json_path = output_dir / "package.json"
|
| 259 |
+
if not package_json_path.exists():
|
| 260 |
+
package_json = {
|
| 261 |
+
"name": "video-api-client",
|
| 262 |
+
"version": "1.0.0",
|
| 263 |
+
"description": "TypeScript client for Video Generation API",
|
| 264 |
+
"main": "dist/index.js",
|
| 265 |
+
"types": "dist/index.d.ts",
|
| 266 |
+
"scripts": {
|
| 267 |
+
"build": "tsc",
|
| 268 |
+
"test": "jest",
|
| 269 |
+
"lint": "eslint src/**/*.ts"
|
| 270 |
+
},
|
| 271 |
+
"dependencies": {
|
| 272 |
+
"node-fetch": "^2.6.7"
|
| 273 |
+
},
|
| 274 |
+
"devDependencies": {
|
| 275 |
+
"typescript": "^4.9.0",
|
| 276 |
+
"@types/node": "^18.0.0",
|
| 277 |
+
"jest": "^29.0.0",
|
| 278 |
+
"@types/jest": "^29.0.0",
|
| 279 |
+
"eslint": "^8.0.0",
|
| 280 |
+
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
| 281 |
+
"@typescript-eslint/parser": "^5.0.0"
|
| 282 |
+
},
|
| 283 |
+
"keywords": ["video", "api", "client", "typescript"],
|
| 284 |
+
"author": "API Team",
|
| 285 |
+
"license": "MIT"
|
| 286 |
+
}
|
| 287 |
+
package_json_path.write_text(json.dumps(package_json, indent=2))
|
| 288 |
+
|
| 289 |
+
def _post_process_python(self, output_dir: Path) -> None:
|
| 290 |
+
"""Post-process Python client."""
|
| 291 |
+
# Create setup.py if it doesn't exist
|
| 292 |
+
setup_py_path = output_dir / "setup.py"
|
| 293 |
+
if not setup_py_path.exists():
|
| 294 |
+
setup_py_content = '''
|
| 295 |
+
from setuptools import setup, find_packages
|
| 296 |
+
|
| 297 |
+
setup(
|
| 298 |
+
name="video-api-client",
|
| 299 |
+
version="1.0.0",
|
| 300 |
+
description="Python client for Video Generation API",
|
| 301 |
+
long_description=open("README.md").read(),
|
| 302 |
+
long_description_content_type="text/markdown",
|
| 303 |
+
author="API Team",
|
| 304 |
+
author_email="[email protected]",
|
| 305 |
+
url="https://github.com/example/video-api-client-python",
|
| 306 |
+
packages=find_packages(),
|
| 307 |
+
install_requires=[
|
| 308 |
+
"requests>=2.25.0",
|
| 309 |
+
"urllib3>=1.26.0",
|
| 310 |
+
"python-dateutil>=2.8.0"
|
| 311 |
+
],
|
| 312 |
+
extras_require={
|
| 313 |
+
"dev": [
|
| 314 |
+
"pytest>=6.0.0",
|
| 315 |
+
"pytest-cov>=2.10.0",
|
| 316 |
+
"black>=21.0.0",
|
| 317 |
+
"flake8>=3.8.0",
|
| 318 |
+
"mypy>=0.800"
|
| 319 |
+
]
|
| 320 |
+
},
|
| 321 |
+
classifiers=[
|
| 322 |
+
"Development Status :: 4 - Beta",
|
| 323 |
+
"Intended Audience :: Developers",
|
| 324 |
+
"License :: OSI Approved :: MIT License",
|
| 325 |
+
"Programming Language :: Python :: 3",
|
| 326 |
+
"Programming Language :: Python :: 3.7",
|
| 327 |
+
"Programming Language :: Python :: 3.8",
|
| 328 |
+
"Programming Language :: Python :: 3.9",
|
| 329 |
+
"Programming Language :: Python :: 3.10",
|
| 330 |
+
"Programming Language :: Python :: 3.11",
|
| 331 |
+
],
|
| 332 |
+
python_requires=">=3.7",
|
| 333 |
+
)
|
| 334 |
+
'''
|
| 335 |
+
setup_py_path.write_text(setup_py_content.strip())
|
| 336 |
+
|
| 337 |
+
def _generate_client_readme(self, language: str) -> str:
|
| 338 |
+
"""Generate README content for client SDK."""
|
| 339 |
+
return f"""# Video Generation API Client - {language.title()}
|
| 340 |
+
|
| 341 |
+
This is an auto-generated client library for the Video Generation API.
|
| 342 |
+
|
| 343 |
+
## Installation
|
| 344 |
+
|
| 345 |
+
### {language.title()}
|
| 346 |
+
{self._get_installation_instructions(language)}
|
| 347 |
+
|
| 348 |
+
## Quick Start
|
| 349 |
+
|
| 350 |
+
```{self._get_code_block_language(language)}
|
| 351 |
+
{self._get_quick_start_example(language)}
|
| 352 |
+
```
|
| 353 |
+
|
| 354 |
+
## Authentication
|
| 355 |
+
|
| 356 |
+
This API uses Clerk authentication. You need to provide a valid Clerk session token:
|
| 357 |
+
|
| 358 |
+
```{self._get_code_block_language(language)}
|
| 359 |
+
{self._get_auth_example(language)}
|
| 360 |
+
```
|
| 361 |
+
|
| 362 |
+
## API Documentation
|
| 363 |
+
|
| 364 |
+
For complete API documentation, visit: https://docs.example.com
|
| 365 |
+
|
| 366 |
+
## Examples
|
| 367 |
+
|
| 368 |
+
See `example.{self._get_file_extension(language)}` for more usage examples.
|
| 369 |
+
|
| 370 |
+
## Support
|
| 371 |
+
|
| 372 |
+
- GitHub Issues: https://github.com/example/video-api-client-{language}/issues
|
| 373 |
+
- Documentation: https://docs.example.com
|
| 374 |
+
- Email: [email protected]
|
| 375 |
+
|
| 376 |
+
## License
|
| 377 |
+
|
| 378 |
+
MIT License - see LICENSE file for details.
|
| 379 |
+
"""
|
| 380 |
+
|
| 381 |
+
def _generate_client_example(self, language: str) -> Optional[str]:
|
| 382 |
+
"""Generate example usage code for client SDK."""
|
| 383 |
+
examples = {
|
| 384 |
+
"typescript": '''
|
| 385 |
+
import { Configuration, VideosApi, JobsApi } from './src';
|
| 386 |
+
|
| 387 |
+
// Configure the client
|
| 388 |
+
const config = new Configuration({
|
| 389 |
+
basePath: 'https://api.example.com',
|
| 390 |
+
accessToken: 'your-clerk-session-token'
|
| 391 |
+
});
|
| 392 |
+
|
| 393 |
+
const videosApi = new VideosApi(config);
|
| 394 |
+
const jobsApi = new JobsApi(config);
|
| 395 |
+
|
| 396 |
+
async function generateVideo() {
|
| 397 |
+
try {
|
| 398 |
+
// Create a video generation job
|
| 399 |
+
const jobResponse = await videosApi.createVideoGenerationJob({
|
| 400 |
+
configuration: {
|
| 401 |
+
topic: "Pythagorean Theorem",
|
| 402 |
+
context: "Explain the mathematical proof with visual demonstration",
|
| 403 |
+
quality: "medium",
|
| 404 |
+
use_rag: true
|
| 405 |
+
}
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
console.log('Job created:', jobResponse.job_id);
|
| 409 |
+
|
| 410 |
+
// Poll for job completion
|
| 411 |
+
let status = 'queued';
|
| 412 |
+
while (status !== 'completed' && status !== 'failed') {
|
| 413 |
+
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
|
| 414 |
+
|
| 415 |
+
const statusResponse = await videosApi.getVideoJobStatus(jobResponse.job_id);
|
| 416 |
+
status = statusResponse.status;
|
| 417 |
+
|
| 418 |
+
console.log(`Job status: ${status} (${statusResponse.progress.percentage}%)`);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
if (status === 'completed') {
|
| 422 |
+
console.log('Video generation completed!');
|
| 423 |
+
// Download the video
|
| 424 |
+
const videoBlob = await videosApi.downloadVideoFile(jobResponse.job_id);
|
| 425 |
+
console.log('Video downloaded');
|
| 426 |
+
} else {
|
| 427 |
+
console.log('Video generation failed');
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
} catch (error) {
|
| 431 |
+
console.error('Error:', error);
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
generateVideo();
|
| 436 |
+
''',
|
| 437 |
+
"python": '''
|
| 438 |
+
import time
|
| 439 |
+
from video_api_client import Configuration, ApiClient, VideosApi, JobsApi
|
| 440 |
+
from video_api_client.models import JobCreateRequest, JobConfiguration
|
| 441 |
+
|
| 442 |
+
# Configure the client
|
| 443 |
+
configuration = Configuration(
|
| 444 |
+
host="https://api.example.com",
|
| 445 |
+
access_token="your-clerk-session-token"
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
with ApiClient(configuration) as api_client:
|
| 449 |
+
videos_api = VideosApi(api_client)
|
| 450 |
+
jobs_api = JobsApi(api_client)
|
| 451 |
+
|
| 452 |
+
try:
|
| 453 |
+
# Create a video generation job
|
| 454 |
+
job_request = JobCreateRequest(
|
| 455 |
+
configuration=JobConfiguration(
|
| 456 |
+
topic="Pythagorean Theorem",
|
| 457 |
+
context="Explain the mathematical proof with visual demonstration",
|
| 458 |
+
quality="medium",
|
| 459 |
+
use_rag=True
|
| 460 |
+
)
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
job_response = videos_api.create_video_generation_job(job_request)
|
| 464 |
+
print(f"Job created: {job_response.job_id}")
|
| 465 |
+
|
| 466 |
+
# Poll for job completion
|
| 467 |
+
status = "queued"
|
| 468 |
+
while status not in ["completed", "failed"]:
|
| 469 |
+
time.sleep(5) # Wait 5 seconds
|
| 470 |
+
|
| 471 |
+
status_response = videos_api.get_video_job_status(job_response.job_id)
|
| 472 |
+
status = status_response.status
|
| 473 |
+
|
| 474 |
+
print(f"Job status: {status} ({status_response.progress.percentage}%)")
|
| 475 |
+
|
| 476 |
+
if status == "completed":
|
| 477 |
+
print("Video generation completed!")
|
| 478 |
+
# Download the video
|
| 479 |
+
video_data = videos_api.download_video_file(job_response.job_id)
|
| 480 |
+
with open("generated_video.mp4", "wb") as f:
|
| 481 |
+
f.write(video_data)
|
| 482 |
+
print("Video downloaded as generated_video.mp4")
|
| 483 |
+
else:
|
| 484 |
+
print("Video generation failed")
|
| 485 |
+
|
| 486 |
+
except Exception as e:
|
| 487 |
+
print(f"Error: {e}")
|
| 488 |
+
''',
|
| 489 |
+
"java": '''
|
| 490 |
+
import com.example.videoapiclient.ApiClient;
|
| 491 |
+
import com.example.videoapiclient.Configuration;
|
| 492 |
+
import com.example.videoapiclient.api.VideosApi;
|
| 493 |
+
import com.example.videoapiclient.api.JobsApi;
|
| 494 |
+
import com.example.videoapiclient.model.*;
|
| 495 |
+
|
| 496 |
+
public class VideoApiExample {
|
| 497 |
+
public static void main(String[] args) {
|
| 498 |
+
// Configure the client
|
| 499 |
+
ApiClient client = Configuration.getDefaultApiClient();
|
| 500 |
+
client.setBasePath("https://api.example.com");
|
| 501 |
+
client.setAccessToken("your-clerk-session-token");
|
| 502 |
+
|
| 503 |
+
VideosApi videosApi = new VideosApi(client);
|
| 504 |
+
JobsApi jobsApi = new JobsApi(client);
|
| 505 |
+
|
| 506 |
+
try {
|
| 507 |
+
// Create a video generation job
|
| 508 |
+
JobConfiguration config = new JobConfiguration()
|
| 509 |
+
.topic("Pythagorean Theorem")
|
| 510 |
+
.context("Explain the mathematical proof with visual demonstration")
|
| 511 |
+
.quality(VideoQuality.MEDIUM)
|
| 512 |
+
.useRag(true);
|
| 513 |
+
|
| 514 |
+
JobCreateRequest request = new JobCreateRequest().configuration(config);
|
| 515 |
+
JobResponse jobResponse = videosApi.createVideoGenerationJob(request);
|
| 516 |
+
|
| 517 |
+
System.out.println("Job created: " + jobResponse.getJobId());
|
| 518 |
+
|
| 519 |
+
// Poll for job completion
|
| 520 |
+
String status = "queued";
|
| 521 |
+
while (!"completed".equals(status) && !"failed".equals(status)) {
|
| 522 |
+
Thread.sleep(5000); // Wait 5 seconds
|
| 523 |
+
|
| 524 |
+
JobStatusResponse statusResponse = videosApi.getVideoJobStatus(jobResponse.getJobId());
|
| 525 |
+
status = statusResponse.getStatus().getValue();
|
| 526 |
+
|
| 527 |
+
System.out.println("Job status: " + status + " (" +
|
| 528 |
+
statusResponse.getProgress().getPercentage() + "%)");
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
if ("completed".equals(status)) {
|
| 532 |
+
System.out.println("Video generation completed!");
|
| 533 |
+
// Download logic would go here
|
| 534 |
+
} else {
|
| 535 |
+
System.out.println("Video generation failed");
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
} catch (Exception e) {
|
| 539 |
+
System.err.println("Error: " + e.getMessage());
|
| 540 |
+
}
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
'''
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
return examples.get(language)
|
| 547 |
+
|
| 548 |
+
def _get_installation_instructions(self, language: str) -> str:
|
| 549 |
+
"""Get installation instructions for each language."""
|
| 550 |
+
instructions = {
|
| 551 |
+
"typescript": "```bash\nnpm install video-api-client\n```",
|
| 552 |
+
"python": "```bash\npip install video-api-client\n```",
|
| 553 |
+
"java": "Add to your `pom.xml`:\n```xml\n<dependency>\n <groupId>com.example</groupId>\n <artifactId>video-api-client</artifactId>\n <version>1.0.0</version>\n</dependency>\n```",
|
| 554 |
+
"csharp": "```bash\ndotnet add package VideoApiClient\n```",
|
| 555 |
+
"go": "```bash\ngo get github.com/example/video-api-client-go\n```",
|
| 556 |
+
"php": "```bash\ncomposer require example/video-api-client\n```",
|
| 557 |
+
"ruby": "```bash\ngem install video_api_client\n```"
|
| 558 |
+
}
|
| 559 |
+
return instructions.get(language, "See documentation for installation instructions.")
|
| 560 |
+
|
| 561 |
+
def _get_code_block_language(self, language: str) -> str:
|
| 562 |
+
"""Get code block language identifier."""
|
| 563 |
+
mapping = {
|
| 564 |
+
"typescript": "typescript",
|
| 565 |
+
"python": "python",
|
| 566 |
+
"java": "java",
|
| 567 |
+
"csharp": "csharp",
|
| 568 |
+
"go": "go",
|
| 569 |
+
"php": "php",
|
| 570 |
+
"ruby": "ruby"
|
| 571 |
+
}
|
| 572 |
+
return mapping.get(language, language)
|
| 573 |
+
|
| 574 |
+
def _get_file_extension(self, language: str) -> str:
|
| 575 |
+
"""Get file extension for each language."""
|
| 576 |
+
extensions = {
|
| 577 |
+
"typescript": "ts",
|
| 578 |
+
"python": "py",
|
| 579 |
+
"java": "java",
|
| 580 |
+
"csharp": "cs",
|
| 581 |
+
"go": "go",
|
| 582 |
+
"php": "php",
|
| 583 |
+
"ruby": "rb"
|
| 584 |
+
}
|
| 585 |
+
return extensions.get(language, "txt")
|
| 586 |
+
|
| 587 |
+
def _get_quick_start_example(self, language: str) -> str:
|
| 588 |
+
"""Get quick start example for each language."""
|
| 589 |
+
examples = {
|
| 590 |
+
"typescript": "import { VideosApi } from 'video-api-client';\n\nconst api = new VideosApi();\nconst job = await api.createVideoGenerationJob({...});",
|
| 591 |
+
"python": "from video_api_client import VideosApi\n\napi = VideosApi()\njob = api.create_video_generation_job(...)",
|
| 592 |
+
"java": "VideosApi api = new VideosApi();\nJobResponse job = api.createVideoGenerationJob(...);",
|
| 593 |
+
"csharp": "var api = new VideosApi();\nvar job = await api.CreateVideoGenerationJobAsync(...);",
|
| 594 |
+
"go": "client := videoapiclient.NewAPIClient(config)\njob, _, err := client.VideosApi.CreateVideoGenerationJob(...)",
|
| 595 |
+
"php": "$api = new VideosApi();\n$job = $api->createVideoGenerationJob(...);",
|
| 596 |
+
"ruby": "api = VideoApiClient::VideosApi.new\njob = api.create_video_generation_job(...)"
|
| 597 |
+
}
|
| 598 |
+
return examples.get(language, "// See documentation for usage examples")
|
| 599 |
+
|
| 600 |
+
def _get_auth_example(self, language: str) -> str:
|
| 601 |
+
"""Get authentication example for each language."""
|
| 602 |
+
examples = {
|
| 603 |
+
"typescript": "const config = new Configuration({\n accessToken: 'your-clerk-session-token'\n});",
|
| 604 |
+
"python": "configuration.access_token = 'your-clerk-session-token'",
|
| 605 |
+
"java": "client.setAccessToken(\"your-clerk-session-token\");",
|
| 606 |
+
"csharp": "Configuration.Default.AccessToken = \"your-clerk-session-token\";",
|
| 607 |
+
"go": "auth := context.WithValue(context.Background(), sw.ContextAccessToken, \"your-clerk-session-token\")",
|
| 608 |
+
"php": "$config->setAccessToken('your-clerk-session-token');",
|
| 609 |
+
"ruby": "VideoApiClient.configure { |c| c.access_token = 'your-clerk-session-token' }"
|
| 610 |
+
}
|
| 611 |
+
return examples.get(language, "// Set your authentication token")
|
| 612 |
+
|
| 613 |
+
def generate_all_clients(self, languages: Optional[List[str]] = None) -> Dict[str, bool]:
|
| 614 |
+
"""Generate client SDKs for all specified languages."""
|
| 615 |
+
if languages is None:
|
| 616 |
+
languages = list(self.SUPPORTED_LANGUAGES.keys())
|
| 617 |
+
|
| 618 |
+
# Ensure OpenAPI spec is available
|
| 619 |
+
if not self.openapi_spec_path:
|
| 620 |
+
self.fetch_openapi_spec()
|
| 621 |
+
|
| 622 |
+
results = {}
|
| 623 |
+
for language in languages:
|
| 624 |
+
if language in self.SUPPORTED_LANGUAGES:
|
| 625 |
+
results[language] = self.generate_client(language)
|
| 626 |
+
else:
|
| 627 |
+
print(f"Skipping unsupported language: {language}")
|
| 628 |
+
results[language] = False
|
| 629 |
+
|
| 630 |
+
return results
|
| 631 |
+
|
| 632 |
+
def cleanup(self):
|
| 633 |
+
"""Clean up temporary files."""
|
| 634 |
+
if self.openapi_spec_path and self.openapi_spec_path.exists():
|
| 635 |
+
self.openapi_spec_path.unlink()
|
| 636 |
+
print(f"Cleaned up temporary OpenAPI spec file")
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
def main():
|
| 640 |
+
"""Main function for command-line usage."""
|
| 641 |
+
parser = argparse.ArgumentParser(description="Generate client SDKs for Video Generation API")
|
| 642 |
+
parser.add_argument(
|
| 643 |
+
"--api-url",
|
| 644 |
+
default="http://localhost:8000",
|
| 645 |
+
help="Base URL of the API (default: http://localhost:8000)"
|
| 646 |
+
)
|
| 647 |
+
parser.add_argument(
|
| 648 |
+
"--output-dir",
|
| 649 |
+
default="generated_clients",
|
| 650 |
+
help="Output directory for generated clients (default: generated_clients)"
|
| 651 |
+
)
|
| 652 |
+
parser.add_argument(
|
| 653 |
+
"--languages",
|
| 654 |
+
nargs="+",
|
| 655 |
+
choices=list(ClientGenerator.SUPPORTED_LANGUAGES.keys()),
|
| 656 |
+
help="Languages to generate (default: all supported languages)"
|
| 657 |
+
)
|
| 658 |
+
parser.add_argument(
|
| 659 |
+
"--install-generator",
|
| 660 |
+
action="store_true",
|
| 661 |
+
help="Install OpenAPI Generator CLI if not found"
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
args = parser.parse_args()
|
| 665 |
+
|
| 666 |
+
generator = ClientGenerator(args.api_url, args.output_dir)
|
| 667 |
+
|
| 668 |
+
# Check if OpenAPI Generator is available
|
| 669 |
+
if not generator.check_openapi_generator():
|
| 670 |
+
if args.install_generator:
|
| 671 |
+
if not generator.install_openapi_generator():
|
| 672 |
+
print("Failed to install OpenAPI Generator. Please install it manually.")
|
| 673 |
+
sys.exit(1)
|
| 674 |
+
else:
|
| 675 |
+
print("OpenAPI Generator CLI not found. Use --install-generator to install it automatically.")
|
| 676 |
+
sys.exit(1)
|
| 677 |
+
|
| 678 |
+
try:
|
| 679 |
+
# Generate clients
|
| 680 |
+
results = generator.generate_all_clients(args.languages)
|
| 681 |
+
|
| 682 |
+
# Print summary
|
| 683 |
+
print("\n" + "="*50)
|
| 684 |
+
print("CLIENT GENERATION SUMMARY")
|
| 685 |
+
print("="*50)
|
| 686 |
+
|
| 687 |
+
successful = []
|
| 688 |
+
failed = []
|
| 689 |
+
|
| 690 |
+
for language, success in results.items():
|
| 691 |
+
if success:
|
| 692 |
+
successful.append(language)
|
| 693 |
+
print(f"✅ {language}: SUCCESS")
|
| 694 |
+
else:
|
| 695 |
+
failed.append(language)
|
| 696 |
+
print(f"❌ {language}: FAILED")
|
| 697 |
+
|
| 698 |
+
print(f"\nSuccessful: {len(successful)}")
|
| 699 |
+
print(f"Failed: {len(failed)}")
|
| 700 |
+
|
| 701 |
+
if successful:
|
| 702 |
+
print(f"\nGenerated clients are available in: {generator.output_base_dir}")
|
| 703 |
+
|
| 704 |
+
# Exit with error code if any generation failed
|
| 705 |
+
sys.exit(1 if failed else 0)
|
| 706 |
+
|
| 707 |
+
finally:
|
| 708 |
+
generator.cleanup()
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
if __name__ == "__main__":
|
| 712 |
+
main()
|
scripts/generate_clients.sh
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Video Generation API Client Generator Script
|
| 4 |
+
# This script generates client SDKs for multiple programming languages
|
| 5 |
+
|
| 6 |
+
set -e # Exit on any error
|
| 7 |
+
|
| 8 |
+
# Configuration
|
| 9 |
+
API_URL="${API_URL:-http://localhost:8000}"
|
| 10 |
+
OUTPUT_DIR="${OUTPUT_DIR:-generated_clients}"
|
| 11 |
+
CONFIG_FILE="openapi-generator-config.yaml"
|
| 12 |
+
|
| 13 |
+
# Colors for output
|
| 14 |
+
RED='\033[0;31m'
|
| 15 |
+
GREEN='\033[0;32m'
|
| 16 |
+
YELLOW='\033[1;33m'
|
| 17 |
+
BLUE='\033[0;34m'
|
| 18 |
+
NC='\033[0m' # No Color
|
| 19 |
+
|
| 20 |
+
# Logging functions
|
| 21 |
+
log_info() {
|
| 22 |
+
echo -e "${BLUE}[INFO]${NC} $1"
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
log_success() {
|
| 26 |
+
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
log_warning() {
|
| 30 |
+
echo -e "${YELLOW}[WARNING]${NC} $1"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
log_error() {
|
| 34 |
+
echo -e "${RED}[ERROR]${NC} $1"
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# Function to check if a command exists
|
| 38 |
+
command_exists() {
|
| 39 |
+
command -v "$1" >/dev/null 2>&1
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# Function to check prerequisites
|
| 43 |
+
check_prerequisites() {
|
| 44 |
+
log_info "Checking prerequisites..."
|
| 45 |
+
|
| 46 |
+
# Check if Node.js and npm are installed
|
| 47 |
+
if ! command_exists node; then
|
| 48 |
+
log_error "Node.js is not installed. Please install Node.js first."
|
| 49 |
+
exit 1
|
| 50 |
+
fi
|
| 51 |
+
|
| 52 |
+
if ! command_exists npm; then
|
| 53 |
+
log_error "npm is not installed. Please install npm first."
|
| 54 |
+
exit 1
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# Check if OpenAPI Generator CLI is installed
|
| 58 |
+
if ! command_exists openapi-generator-cli; then
|
| 59 |
+
log_warning "OpenAPI Generator CLI not found. Installing..."
|
| 60 |
+
npm install -g @openapitools/openapi-generator-cli
|
| 61 |
+
|
| 62 |
+
if ! command_exists openapi-generator-cli; then
|
| 63 |
+
log_error "Failed to install OpenAPI Generator CLI"
|
| 64 |
+
exit 1
|
| 65 |
+
fi
|
| 66 |
+
log_success "OpenAPI Generator CLI installed successfully"
|
| 67 |
+
else
|
| 68 |
+
log_success "OpenAPI Generator CLI found"
|
| 69 |
+
fi
|
| 70 |
+
|
| 71 |
+
# Check if Python is available (for the Python script)
|
| 72 |
+
if ! command_exists python3; then
|
| 73 |
+
log_warning "Python 3 not found. Some features may not work."
|
| 74 |
+
fi
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# Function to fetch OpenAPI specification
|
| 78 |
+
fetch_openapi_spec() {
|
| 79 |
+
log_info "Fetching OpenAPI specification from $API_URL..."
|
| 80 |
+
|
| 81 |
+
# Create temporary directory
|
| 82 |
+
TEMP_DIR=$(mktemp -d)
|
| 83 |
+
OPENAPI_SPEC="$TEMP_DIR/openapi.json"
|
| 84 |
+
|
| 85 |
+
# Fetch the OpenAPI spec
|
| 86 |
+
if curl -s -f "$API_URL/openapi.json" -o "$OPENAPI_SPEC"; then
|
| 87 |
+
log_success "OpenAPI specification fetched successfully"
|
| 88 |
+
echo "$OPENAPI_SPEC"
|
| 89 |
+
else
|
| 90 |
+
log_error "Failed to fetch OpenAPI specification from $API_URL"
|
| 91 |
+
log_error "Make sure the API server is running and accessible"
|
| 92 |
+
exit 1
|
| 93 |
+
fi
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
# Function to generate client for a specific language
|
| 97 |
+
generate_client() {
|
| 98 |
+
local language=$1
|
| 99 |
+
local spec_file=$2
|
| 100 |
+
|
| 101 |
+
log_info "Generating $language client..."
|
| 102 |
+
|
| 103 |
+
case $language in
|
| 104 |
+
"typescript")
|
| 105 |
+
openapi-generator-cli generate \
|
| 106 |
+
-i "$spec_file" \
|
| 107 |
+
-g typescript-fetch \
|
| 108 |
+
-o "$OUTPUT_DIR/typescript" \
|
| 109 |
+
--package-name video-api-client \
|
| 110 |
+
--additional-properties=npmName=video-api-client,npmVersion=1.0.0,supportsES6=true,withInterfaces=true,typescriptThreePlus=true
|
| 111 |
+
;;
|
| 112 |
+
"python")
|
| 113 |
+
openapi-generator-cli generate \
|
| 114 |
+
-i "$spec_file" \
|
| 115 |
+
-g python \
|
| 116 |
+
-o "$OUTPUT_DIR/python" \
|
| 117 |
+
--package-name video_api_client \
|
| 118 |
+
--additional-properties=packageName=video_api_client,projectName=video-api-client,packageVersion=1.0.0
|
| 119 |
+
;;
|
| 120 |
+
"java")
|
| 121 |
+
openapi-generator-cli generate \
|
| 122 |
+
-i "$spec_file" \
|
| 123 |
+
-g java \
|
| 124 |
+
-o "$OUTPUT_DIR/java" \
|
| 125 |
+
--package-name com.example.videoapiclient \
|
| 126 |
+
--additional-properties=groupId=com.example,artifactId=video-api-client,artifactVersion=1.0.0,library=okhttp-gson,java8=true
|
| 127 |
+
;;
|
| 128 |
+
"csharp")
|
| 129 |
+
openapi-generator-cli generate \
|
| 130 |
+
-i "$spec_file" \
|
| 131 |
+
-g csharp \
|
| 132 |
+
-o "$OUTPUT_DIR/csharp" \
|
| 133 |
+
--package-name VideoApiClient \
|
| 134 |
+
--additional-properties=packageName=VideoApiClient,packageVersion=1.0.0,clientPackage=VideoApiClient,targetFramework=netstandard2.0
|
| 135 |
+
;;
|
| 136 |
+
"go")
|
| 137 |
+
openapi-generator-cli generate \
|
| 138 |
+
-i "$spec_file" \
|
| 139 |
+
-g go \
|
| 140 |
+
-o "$OUTPUT_DIR/go" \
|
| 141 |
+
--package-name videoapiclient \
|
| 142 |
+
--additional-properties=packageName=videoapiclient,packageVersion=1.0.0,packageUrl=github.com/example/video-api-client-go
|
| 143 |
+
;;
|
| 144 |
+
"php")
|
| 145 |
+
openapi-generator-cli generate \
|
| 146 |
+
-i "$spec_file" \
|
| 147 |
+
-g php \
|
| 148 |
+
-o "$OUTPUT_DIR/php" \
|
| 149 |
+
--package-name VideoApiClient \
|
| 150 |
+
--additional-properties=packageName=VideoApiClient,composerVendorName=example,composerProjectName=video-api-client,packageVersion=1.0.0
|
| 151 |
+
;;
|
| 152 |
+
"ruby")
|
| 153 |
+
openapi-generator-cli generate \
|
| 154 |
+
-i "$spec_file" \
|
| 155 |
+
-g ruby \
|
| 156 |
+
-o "$OUTPUT_DIR/ruby" \
|
| 157 |
+
--package-name video_api_client \
|
| 158 |
+
--additional-properties=gemName=video_api_client,gemVersion=1.0.0,moduleName=VideoApiClient
|
| 159 |
+
;;
|
| 160 |
+
*)
|
| 161 |
+
log_error "Unsupported language: $language"
|
| 162 |
+
return 1
|
| 163 |
+
;;
|
| 164 |
+
esac
|
| 165 |
+
|
| 166 |
+
if [ $? -eq 0 ]; then
|
| 167 |
+
log_success "$language client generated successfully"
|
| 168 |
+
post_process_client "$language"
|
| 169 |
+
return 0
|
| 170 |
+
else
|
| 171 |
+
log_error "Failed to generate $language client"
|
| 172 |
+
return 1
|
| 173 |
+
fi
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
# Function to post-process generated clients
|
| 177 |
+
post_process_client() {
|
| 178 |
+
local language=$1
|
| 179 |
+
local client_dir="$OUTPUT_DIR/$language"
|
| 180 |
+
|
| 181 |
+
log_info "Post-processing $language client..."
|
| 182 |
+
|
| 183 |
+
# Create README if it doesn't exist
|
| 184 |
+
if [ ! -f "$client_dir/README.md" ]; then
|
| 185 |
+
create_readme "$language" "$client_dir"
|
| 186 |
+
fi
|
| 187 |
+
|
| 188 |
+
# Create example file
|
| 189 |
+
create_example "$language" "$client_dir"
|
| 190 |
+
|
| 191 |
+
# Language-specific post-processing
|
| 192 |
+
case $language in
|
| 193 |
+
"typescript")
|
| 194 |
+
post_process_typescript "$client_dir"
|
| 195 |
+
;;
|
| 196 |
+
"python")
|
| 197 |
+
post_process_python "$client_dir"
|
| 198 |
+
;;
|
| 199 |
+
"java")
|
| 200 |
+
post_process_java "$client_dir"
|
| 201 |
+
;;
|
| 202 |
+
esac
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
# Function to create README for client
|
| 206 |
+
create_readme() {
|
| 207 |
+
local language=$1
|
| 208 |
+
local client_dir=$2
|
| 209 |
+
|
| 210 |
+
cat > "$client_dir/README.md" << EOF
|
| 211 |
+
# Video Generation API Client - ${language^}
|
| 212 |
+
|
| 213 |
+
This is an auto-generated client library for the Video Generation API.
|
| 214 |
+
|
| 215 |
+
## Installation
|
| 216 |
+
|
| 217 |
+
See the installation instructions in the main documentation.
|
| 218 |
+
|
| 219 |
+
## Quick Start
|
| 220 |
+
|
| 221 |
+
\`\`\`${language}
|
| 222 |
+
// See example.${language} for usage examples
|
| 223 |
+
\`\`\`
|
| 224 |
+
|
| 225 |
+
## Authentication
|
| 226 |
+
|
| 227 |
+
This API uses Clerk authentication. You need to provide a valid Clerk session token.
|
| 228 |
+
|
| 229 |
+
## Documentation
|
| 230 |
+
|
| 231 |
+
For complete API documentation, visit: https://docs.example.com
|
| 232 |
+
|
| 233 |
+
## Support
|
| 234 |
+
|
| 235 |
+
- GitHub Issues: https://github.com/example/video-api-client-${language}/issues
|
| 236 |
+
- Documentation: https://docs.example.com
|
| 237 |
+
- Email: [email protected]
|
| 238 |
+
|
| 239 |
+
## License
|
| 240 |
+
|
| 241 |
+
MIT License - see LICENSE file for details.
|
| 242 |
+
EOF
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
# Function to create example file
|
| 246 |
+
create_example() {
|
| 247 |
+
local language=$1
|
| 248 |
+
local client_dir=$2
|
| 249 |
+
|
| 250 |
+
case $language in
|
| 251 |
+
"typescript")
|
| 252 |
+
cat > "$client_dir/example.ts" << 'EOF'
|
| 253 |
+
import { Configuration, VideosApi } from './src';
|
| 254 |
+
|
| 255 |
+
const config = new Configuration({
|
| 256 |
+
basePath: 'https://api.example.com',
|
| 257 |
+
accessToken: 'your-clerk-session-token'
|
| 258 |
+
});
|
| 259 |
+
|
| 260 |
+
const videosApi = new VideosApi(config);
|
| 261 |
+
|
| 262 |
+
async function generateVideo() {
|
| 263 |
+
try {
|
| 264 |
+
const jobResponse = await videosApi.createVideoGenerationJob({
|
| 265 |
+
configuration: {
|
| 266 |
+
topic: "Pythagorean Theorem",
|
| 267 |
+
context: "Explain the mathematical proof",
|
| 268 |
+
quality: "medium",
|
| 269 |
+
use_rag: true
|
| 270 |
+
}
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
console.log('Job created:', jobResponse.job_id);
|
| 274 |
+
} catch (error) {
|
| 275 |
+
console.error('Error:', error);
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
generateVideo();
|
| 280 |
+
EOF
|
| 281 |
+
;;
|
| 282 |
+
"python")
|
| 283 |
+
cat > "$client_dir/example.py" << 'EOF'
|
| 284 |
+
from video_api_client import Configuration, ApiClient, VideosApi
|
| 285 |
+
from video_api_client.models import JobCreateRequest, JobConfiguration
|
| 286 |
+
|
| 287 |
+
configuration = Configuration(
|
| 288 |
+
host="https://api.example.com",
|
| 289 |
+
access_token="your-clerk-session-token"
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
with ApiClient(configuration) as api_client:
|
| 293 |
+
videos_api = VideosApi(api_client)
|
| 294 |
+
|
| 295 |
+
try:
|
| 296 |
+
job_request = JobCreateRequest(
|
| 297 |
+
configuration=JobConfiguration(
|
| 298 |
+
topic="Pythagorean Theorem",
|
| 299 |
+
context="Explain the mathematical proof",
|
| 300 |
+
quality="medium",
|
| 301 |
+
use_rag=True
|
| 302 |
+
)
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
job_response = videos_api.create_video_generation_job(job_request)
|
| 306 |
+
print(f"Job created: {job_response.job_id}")
|
| 307 |
+
|
| 308 |
+
except Exception as e:
|
| 309 |
+
print(f"Error: {e}")
|
| 310 |
+
EOF
|
| 311 |
+
;;
|
| 312 |
+
esac
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
# Language-specific post-processing functions
|
| 316 |
+
post_process_typescript() {
|
| 317 |
+
local client_dir=$1
|
| 318 |
+
|
| 319 |
+
# Create package.json if it doesn't exist
|
| 320 |
+
if [ ! -f "$client_dir/package.json" ]; then
|
| 321 |
+
cat > "$client_dir/package.json" << 'EOF'
|
| 322 |
+
{
|
| 323 |
+
"name": "video-api-client",
|
| 324 |
+
"version": "1.0.0",
|
| 325 |
+
"description": "TypeScript client for Video Generation API",
|
| 326 |
+
"main": "dist/index.js",
|
| 327 |
+
"types": "dist/index.d.ts",
|
| 328 |
+
"scripts": {
|
| 329 |
+
"build": "tsc",
|
| 330 |
+
"test": "jest"
|
| 331 |
+
},
|
| 332 |
+
"dependencies": {
|
| 333 |
+
"node-fetch": "^2.6.7"
|
| 334 |
+
},
|
| 335 |
+
"devDependencies": {
|
| 336 |
+
"typescript": "^4.9.0",
|
| 337 |
+
"@types/node": "^18.0.0"
|
| 338 |
+
},
|
| 339 |
+
"keywords": ["video", "api", "client", "typescript"],
|
| 340 |
+
"author": "API Team",
|
| 341 |
+
"license": "MIT"
|
| 342 |
+
}
|
| 343 |
+
EOF
|
| 344 |
+
fi
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
post_process_python() {
|
| 348 |
+
local client_dir=$1
|
| 349 |
+
|
| 350 |
+
# Create setup.py if it doesn't exist
|
| 351 |
+
if [ ! -f "$client_dir/setup.py" ]; then
|
| 352 |
+
cat > "$client_dir/setup.py" << 'EOF'
|
| 353 |
+
from setuptools import setup, find_packages
|
| 354 |
+
|
| 355 |
+
setup(
|
| 356 |
+
name="video-api-client",
|
| 357 |
+
version="1.0.0",
|
| 358 |
+
description="Python client for Video Generation API",
|
| 359 |
+
packages=find_packages(),
|
| 360 |
+
install_requires=[
|
| 361 |
+
"requests>=2.25.0",
|
| 362 |
+
"urllib3>=1.26.0",
|
| 363 |
+
"python-dateutil>=2.8.0"
|
| 364 |
+
],
|
| 365 |
+
author="API Team",
|
| 366 |
+
author_email="[email protected]",
|
| 367 |
+
license="MIT"
|
| 368 |
+
)
|
| 369 |
+
EOF
|
| 370 |
+
fi
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
post_process_java() {
|
| 374 |
+
local client_dir=$1
|
| 375 |
+
|
| 376 |
+
# Java post-processing would go here
|
| 377 |
+
log_info "Java client post-processing completed"
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
# Function to test generated clients
|
| 381 |
+
test_clients() {
|
| 382 |
+
log_info "Testing generated clients..."
|
| 383 |
+
|
| 384 |
+
# Test TypeScript client
|
| 385 |
+
if [ -d "$OUTPUT_DIR/typescript" ]; then
|
| 386 |
+
log_info "Testing TypeScript client..."
|
| 387 |
+
cd "$OUTPUT_DIR/typescript"
|
| 388 |
+
if [ -f "package.json" ]; then
|
| 389 |
+
npm install --silent
|
| 390 |
+
npm run build --silent 2>/dev/null || log_warning "TypeScript build failed"
|
| 391 |
+
fi
|
| 392 |
+
cd - > /dev/null
|
| 393 |
+
fi
|
| 394 |
+
|
| 395 |
+
# Test Python client
|
| 396 |
+
if [ -d "$OUTPUT_DIR/python" ]; then
|
| 397 |
+
log_info "Testing Python client..."
|
| 398 |
+
cd "$OUTPUT_DIR/python"
|
| 399 |
+
if [ -f "setup.py" ] && command_exists python3; then
|
| 400 |
+
python3 setup.py check --quiet 2>/dev/null || log_warning "Python setup check failed"
|
| 401 |
+
fi
|
| 402 |
+
cd - > /dev/null
|
| 403 |
+
fi
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
# Function to create documentation
|
| 407 |
+
create_documentation() {
|
| 408 |
+
log_info "Creating client documentation..."
|
| 409 |
+
|
| 410 |
+
cat > "$OUTPUT_DIR/README.md" << EOF
|
| 411 |
+
# Video Generation API Clients
|
| 412 |
+
|
| 413 |
+
This directory contains auto-generated client libraries for the Video Generation API in multiple programming languages.
|
| 414 |
+
|
| 415 |
+
## Available Clients
|
| 416 |
+
|
| 417 |
+
EOF
|
| 418 |
+
|
| 419 |
+
for dir in "$OUTPUT_DIR"/*; do
|
| 420 |
+
if [ -d "$dir" ]; then
|
| 421 |
+
language=$(basename "$dir")
|
| 422 |
+
echo "- [$language](./$language/)" >> "$OUTPUT_DIR/README.md"
|
| 423 |
+
fi
|
| 424 |
+
done
|
| 425 |
+
|
| 426 |
+
cat >> "$OUTPUT_DIR/README.md" << EOF
|
| 427 |
+
|
| 428 |
+
## Quick Start
|
| 429 |
+
|
| 430 |
+
Each client directory contains:
|
| 431 |
+
- Generated API client code
|
| 432 |
+
- README with installation and usage instructions
|
| 433 |
+
- Example code demonstrating basic usage
|
| 434 |
+
- Package/project files for the respective language
|
| 435 |
+
|
| 436 |
+
## Authentication
|
| 437 |
+
|
| 438 |
+
All clients support Clerk authentication. Set your session token when configuring the client.
|
| 439 |
+
|
| 440 |
+
## Documentation
|
| 441 |
+
|
| 442 |
+
- API Documentation: https://docs.example.com
|
| 443 |
+
- OpenAPI Specification: $API_URL/openapi.json
|
| 444 |
+
|
| 445 |
+
## Support
|
| 446 |
+
|
| 447 |
+
For issues with the generated clients:
|
| 448 |
+
- Check the individual client README files
|
| 449 |
+
- Visit our documentation at https://docs.example.com
|
| 450 |
+
- Contact support at [email protected]
|
| 451 |
+
|
| 452 |
+
Generated on: $(date)
|
| 453 |
+
EOF
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
# Main function
|
| 457 |
+
main() {
|
| 458 |
+
local languages=("$@")
|
| 459 |
+
|
| 460 |
+
# Default to all languages if none specified
|
| 461 |
+
if [ ${#languages[@]} -eq 0 ]; then
|
| 462 |
+
languages=("typescript" "python" "java" "csharp" "go" "php" "ruby")
|
| 463 |
+
fi
|
| 464 |
+
|
| 465 |
+
log_info "Starting client generation for languages: ${languages[*]}"
|
| 466 |
+
|
| 467 |
+
# Check prerequisites
|
| 468 |
+
check_prerequisites
|
| 469 |
+
|
| 470 |
+
# Fetch OpenAPI specification
|
| 471 |
+
OPENAPI_SPEC=$(fetch_openapi_spec)
|
| 472 |
+
|
| 473 |
+
# Create output directory
|
| 474 |
+
mkdir -p "$OUTPUT_DIR"
|
| 475 |
+
|
| 476 |
+
# Generate clients
|
| 477 |
+
local successful=()
|
| 478 |
+
local failed=()
|
| 479 |
+
|
| 480 |
+
for language in "${languages[@]}"; do
|
| 481 |
+
if generate_client "$language" "$OPENAPI_SPEC"; then
|
| 482 |
+
successful+=("$language")
|
| 483 |
+
else
|
| 484 |
+
failed+=("$language")
|
| 485 |
+
fi
|
| 486 |
+
done
|
| 487 |
+
|
| 488 |
+
# Test clients
|
| 489 |
+
test_clients
|
| 490 |
+
|
| 491 |
+
# Create documentation
|
| 492 |
+
create_documentation
|
| 493 |
+
|
| 494 |
+
# Print summary
|
| 495 |
+
echo
|
| 496 |
+
echo "=================================================="
|
| 497 |
+
echo "CLIENT GENERATION SUMMARY"
|
| 498 |
+
echo "=================================================="
|
| 499 |
+
|
| 500 |
+
if [ ${#successful[@]} -gt 0 ]; then
|
| 501 |
+
log_success "Successfully generated: ${successful[*]}"
|
| 502 |
+
fi
|
| 503 |
+
|
| 504 |
+
if [ ${#failed[@]} -gt 0 ]; then
|
| 505 |
+
log_error "Failed to generate: ${failed[*]}"
|
| 506 |
+
fi
|
| 507 |
+
|
| 508 |
+
echo
|
| 509 |
+
log_info "Generated clients are available in: $OUTPUT_DIR"
|
| 510 |
+
|
| 511 |
+
# Cleanup
|
| 512 |
+
rm -rf "$(dirname "$OPENAPI_SPEC")"
|
| 513 |
+
|
| 514 |
+
# Exit with error if any generation failed
|
| 515 |
+
if [ ${#failed[@]} -gt 0 ]; then
|
| 516 |
+
exit 1
|
| 517 |
+
fi
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
# Parse command line arguments
|
| 521 |
+
while [[ $# -gt 0 ]]; do
|
| 522 |
+
case $1 in
|
| 523 |
+
--api-url)
|
| 524 |
+
API_URL="$2"
|
| 525 |
+
shift 2
|
| 526 |
+
;;
|
| 527 |
+
--output-dir)
|
| 528 |
+
OUTPUT_DIR="$2"
|
| 529 |
+
shift 2
|
| 530 |
+
;;
|
| 531 |
+
--help)
|
| 532 |
+
echo "Usage: $0 [OPTIONS] [LANGUAGES...]"
|
| 533 |
+
echo ""
|
| 534 |
+
echo "Generate client SDKs for Video Generation API"
|
| 535 |
+
echo ""
|
| 536 |
+
echo "Options:"
|
| 537 |
+
echo " --api-url URL API base URL (default: http://localhost:8000)"
|
| 538 |
+
echo " --output-dir DIR Output directory (default: generated_clients)"
|
| 539 |
+
echo " --help Show this help message"
|
| 540 |
+
echo ""
|
| 541 |
+
echo "Languages:"
|
| 542 |
+
echo " typescript python java csharp go php ruby"
|
| 543 |
+
echo ""
|
| 544 |
+
echo "Examples:"
|
| 545 |
+
echo " $0 # Generate all clients"
|
| 546 |
+
echo " $0 typescript python # Generate only TypeScript and Python"
|
| 547 |
+
echo " $0 --api-url https://api.example.com typescript"
|
| 548 |
+
exit 0
|
| 549 |
+
;;
|
| 550 |
+
*)
|
| 551 |
+
break
|
| 552 |
+
;;
|
| 553 |
+
esac
|
| 554 |
+
done
|
| 555 |
+
|
| 556 |
+
# Run main function with remaining arguments as languages
|
| 557 |
+
main "$@"
|
scripts/setup.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Development setup script.
|
| 4 |
+
Creates necessary directories and validates configuration.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def create_directories():
|
| 13 |
+
"""Create necessary directories for the application."""
|
| 14 |
+
directories = [
|
| 15 |
+
"uploads",
|
| 16 |
+
"videos",
|
| 17 |
+
"logs",
|
| 18 |
+
"data",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
for directory in directories:
|
| 22 |
+
Path(directory).mkdir(exist_ok=True)
|
| 23 |
+
print(f"✓ Created directory: {directory}")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def check_env_file():
|
| 27 |
+
"""Check if .env file exists and create from example if not."""
|
| 28 |
+
env_file = Path(".env")
|
| 29 |
+
env_example = Path(".env.example")
|
| 30 |
+
|
| 31 |
+
if not env_file.exists():
|
| 32 |
+
if env_example.exists():
|
| 33 |
+
# Copy example to .env
|
| 34 |
+
env_file.write_text(env_example.read_text())
|
| 35 |
+
print("✓ Created .env file from .env.example")
|
| 36 |
+
print("⚠️ Please update the .env file with your actual configuration values")
|
| 37 |
+
else:
|
| 38 |
+
print("❌ .env.example file not found")
|
| 39 |
+
return False
|
| 40 |
+
else:
|
| 41 |
+
print("✓ .env file already exists")
|
| 42 |
+
|
| 43 |
+
return True
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def validate_required_env_vars():
|
| 47 |
+
"""Validate that required environment variables are set."""
|
| 48 |
+
from dotenv import load_dotenv
|
| 49 |
+
|
| 50 |
+
load_dotenv()
|
| 51 |
+
|
| 52 |
+
required_vars = [
|
| 53 |
+
"CLERK_SECRET_KEY",
|
| 54 |
+
"CLERK_PUBLISHABLE_KEY",
|
| 55 |
+
"SECRET_KEY",
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
missing_vars = []
|
| 59 |
+
for var in required_vars:
|
| 60 |
+
if not os.getenv(var) or os.getenv(var) == f"your_{var.lower()}_here":
|
| 61 |
+
missing_vars.append(var)
|
| 62 |
+
|
| 63 |
+
if missing_vars:
|
| 64 |
+
print("❌ Missing required environment variables:")
|
| 65 |
+
for var in missing_vars:
|
| 66 |
+
print(f" - {var}")
|
| 67 |
+
print("\nPlease update your .env file with actual values.")
|
| 68 |
+
return False
|
| 69 |
+
|
| 70 |
+
print("✓ All required environment variables are set")
|
| 71 |
+
return True
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def main():
|
| 75 |
+
"""Main setup function."""
|
| 76 |
+
print("🚀 Setting up FastAPI Video Backend development environment...\n")
|
| 77 |
+
|
| 78 |
+
# Create directories
|
| 79 |
+
create_directories()
|
| 80 |
+
print()
|
| 81 |
+
|
| 82 |
+
# Check .env file
|
| 83 |
+
if not check_env_file():
|
| 84 |
+
sys.exit(1)
|
| 85 |
+
print()
|
| 86 |
+
|
| 87 |
+
# Validate environment variables
|
| 88 |
+
try:
|
| 89 |
+
if not validate_required_env_vars():
|
| 90 |
+
print("\n⚠️ Setup completed with warnings. Please fix the issues above.")
|
| 91 |
+
sys.exit(1)
|
| 92 |
+
except ImportError:
|
| 93 |
+
print("⚠️ python-dotenv not installed. Skipping environment validation.")
|
| 94 |
+
print(" Install dependencies with: pip install -e .")
|
| 95 |
+
|
| 96 |
+
print("\n✅ Development environment setup completed!")
|
| 97 |
+
print("\nNext steps:")
|
| 98 |
+
print("1. Install dependencies: pip install -e .")
|
| 99 |
+
print("2. Update .env file with your actual configuration values")
|
| 100 |
+
print("3. Start Redis server: redis-server")
|
| 101 |
+
print("4. Run the application: python -m src.app.main")
|
| 102 |
+
print("5. Visit http://localhost:8000/docs for API documentation")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
main()
|
scripts/test_clients.py
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Client SDK testing script for FastAPI Video Generation Backend.
|
| 4 |
+
|
| 5 |
+
This script tests the generated client SDKs to ensure they work correctly
|
| 6 |
+
with the API endpoints. It performs basic functionality tests and validates
|
| 7 |
+
the client-server communication.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import json
|
| 13 |
+
import time
|
| 14 |
+
import asyncio
|
| 15 |
+
import requests
|
| 16 |
+
import subprocess
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Dict, List, Optional, Any
|
| 19 |
+
import tempfile
|
| 20 |
+
import logging
|
| 21 |
+
|
| 22 |
+
# Set up logging
|
| 23 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ClientTester:
|
| 28 |
+
"""Test suite for generated client SDKs."""
|
| 29 |
+
|
| 30 |
+
def __init__(self, api_url: str = "http://localhost:8000", clients_dir: str = "generated_clients"):
|
| 31 |
+
self.api_url = api_url.rstrip("/")
|
| 32 |
+
self.clients_dir = Path(clients_dir)
|
| 33 |
+
self.test_results: Dict[str, Dict[str, Any]] = {}
|
| 34 |
+
|
| 35 |
+
def check_api_availability(self) -> bool:
|
| 36 |
+
"""Check if the API server is running and accessible."""
|
| 37 |
+
try:
|
| 38 |
+
logger.info(f"Checking API availability at {self.api_url}")
|
| 39 |
+
response = requests.get(f"{self.api_url}/health", timeout=10)
|
| 40 |
+
response.raise_for_status()
|
| 41 |
+
logger.info("API server is available")
|
| 42 |
+
return True
|
| 43 |
+
except requests.RequestException as e:
|
| 44 |
+
logger.error(f"API server is not available: {e}")
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
def test_openapi_spec(self) -> bool:
|
| 48 |
+
"""Test if OpenAPI specification is accessible."""
|
| 49 |
+
try:
|
| 50 |
+
logger.info("Testing OpenAPI specification accessibility")
|
| 51 |
+
response = requests.get(f"{self.api_url}/openapi.json", timeout=10)
|
| 52 |
+
response.raise_for_status()
|
| 53 |
+
|
| 54 |
+
spec = response.json()
|
| 55 |
+
|
| 56 |
+
# Validate basic OpenAPI structure
|
| 57 |
+
required_fields = ["openapi", "info", "paths"]
|
| 58 |
+
for field in required_fields:
|
| 59 |
+
if field not in spec:
|
| 60 |
+
logger.error(f"OpenAPI spec missing required field: {field}")
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
logger.info("OpenAPI specification is valid")
|
| 64 |
+
return True
|
| 65 |
+
|
| 66 |
+
except requests.RequestException as e:
|
| 67 |
+
logger.error(f"Failed to fetch OpenAPI specification: {e}")
|
| 68 |
+
return False
|
| 69 |
+
except json.JSONDecodeError as e:
|
| 70 |
+
logger.error(f"Invalid JSON in OpenAPI specification: {e}")
|
| 71 |
+
return False
|
| 72 |
+
|
| 73 |
+
def test_typescript_client(self) -> Dict[str, Any]:
|
| 74 |
+
"""Test TypeScript client."""
|
| 75 |
+
client_dir = self.clients_dir / "typescript"
|
| 76 |
+
result = {
|
| 77 |
+
"language": "typescript",
|
| 78 |
+
"exists": False,
|
| 79 |
+
"builds": False,
|
| 80 |
+
"has_examples": False,
|
| 81 |
+
"has_docs": False,
|
| 82 |
+
"errors": []
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
if not client_dir.exists():
|
| 87 |
+
result["errors"].append("Client directory does not exist")
|
| 88 |
+
return result
|
| 89 |
+
|
| 90 |
+
result["exists"] = True
|
| 91 |
+
|
| 92 |
+
# Check for essential files
|
| 93 |
+
essential_files = ["package.json", "README.md"]
|
| 94 |
+
for file in essential_files:
|
| 95 |
+
if not (client_dir / file).exists():
|
| 96 |
+
result["errors"].append(f"Missing {file}")
|
| 97 |
+
|
| 98 |
+
# Check for example file
|
| 99 |
+
if (client_dir / "example.ts").exists():
|
| 100 |
+
result["has_examples"] = True
|
| 101 |
+
|
| 102 |
+
# Check for documentation
|
| 103 |
+
if (client_dir / "README.md").exists():
|
| 104 |
+
result["has_docs"] = True
|
| 105 |
+
|
| 106 |
+
# Try to build the client
|
| 107 |
+
if (client_dir / "package.json").exists():
|
| 108 |
+
logger.info("Testing TypeScript client build")
|
| 109 |
+
|
| 110 |
+
# Install dependencies
|
| 111 |
+
install_result = subprocess.run(
|
| 112 |
+
["npm", "install"],
|
| 113 |
+
cwd=client_dir,
|
| 114 |
+
capture_output=True,
|
| 115 |
+
text=True,
|
| 116 |
+
timeout=120
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
if install_result.returncode != 0:
|
| 120 |
+
result["errors"].append(f"npm install failed: {install_result.stderr}")
|
| 121 |
+
else:
|
| 122 |
+
# Try to build
|
| 123 |
+
build_result = subprocess.run(
|
| 124 |
+
["npm", "run", "build"],
|
| 125 |
+
cwd=client_dir,
|
| 126 |
+
capture_output=True,
|
| 127 |
+
text=True,
|
| 128 |
+
timeout=60
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
if build_result.returncode == 0:
|
| 132 |
+
result["builds"] = True
|
| 133 |
+
else:
|
| 134 |
+
result["errors"].append(f"Build failed: {build_result.stderr}")
|
| 135 |
+
|
| 136 |
+
except subprocess.TimeoutExpired:
|
| 137 |
+
result["errors"].append("Build timeout")
|
| 138 |
+
except Exception as e:
|
| 139 |
+
result["errors"].append(f"Unexpected error: {str(e)}")
|
| 140 |
+
|
| 141 |
+
return result
|
| 142 |
+
|
| 143 |
+
def test_python_client(self) -> Dict[str, Any]:
|
| 144 |
+
"""Test Python client."""
|
| 145 |
+
client_dir = self.clients_dir / "python"
|
| 146 |
+
result = {
|
| 147 |
+
"language": "python",
|
| 148 |
+
"exists": False,
|
| 149 |
+
"installs": False,
|
| 150 |
+
"imports": False,
|
| 151 |
+
"has_examples": False,
|
| 152 |
+
"has_docs": False,
|
| 153 |
+
"errors": []
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
try:
|
| 157 |
+
if not client_dir.exists():
|
| 158 |
+
result["errors"].append("Client directory does not exist")
|
| 159 |
+
return result
|
| 160 |
+
|
| 161 |
+
result["exists"] = True
|
| 162 |
+
|
| 163 |
+
# Check for essential files
|
| 164 |
+
essential_files = ["setup.py", "README.md"]
|
| 165 |
+
for file in essential_files:
|
| 166 |
+
if not (client_dir / file).exists():
|
| 167 |
+
result["errors"].append(f"Missing {file}")
|
| 168 |
+
|
| 169 |
+
# Check for example file
|
| 170 |
+
if (client_dir / "example.py").exists():
|
| 171 |
+
result["has_examples"] = True
|
| 172 |
+
|
| 173 |
+
# Check for documentation
|
| 174 |
+
if (client_dir / "README.md").exists():
|
| 175 |
+
result["has_docs"] = True
|
| 176 |
+
|
| 177 |
+
# Try to install the client in development mode
|
| 178 |
+
if (client_dir / "setup.py").exists():
|
| 179 |
+
logger.info("Testing Python client installation")
|
| 180 |
+
|
| 181 |
+
install_result = subprocess.run(
|
| 182 |
+
[sys.executable, "setup.py", "develop", "--user"],
|
| 183 |
+
cwd=client_dir,
|
| 184 |
+
capture_output=True,
|
| 185 |
+
text=True,
|
| 186 |
+
timeout=120
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
if install_result.returncode == 0:
|
| 190 |
+
result["installs"] = True
|
| 191 |
+
|
| 192 |
+
# Try to import the client
|
| 193 |
+
try:
|
| 194 |
+
import importlib.util
|
| 195 |
+
|
| 196 |
+
# Find the main module
|
| 197 |
+
main_module_path = None
|
| 198 |
+
for py_file in client_dir.rglob("*.py"):
|
| 199 |
+
if "__init__.py" in py_file.name:
|
| 200 |
+
main_module_path = py_file
|
| 201 |
+
break
|
| 202 |
+
|
| 203 |
+
if main_module_path:
|
| 204 |
+
spec = importlib.util.spec_from_file_location("test_client", main_module_path)
|
| 205 |
+
if spec and spec.loader:
|
| 206 |
+
module = importlib.util.module_from_spec(spec)
|
| 207 |
+
spec.loader.exec_module(module)
|
| 208 |
+
result["imports"] = True
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
result["errors"].append(f"Import failed: {str(e)}")
|
| 212 |
+
else:
|
| 213 |
+
result["errors"].append(f"Installation failed: {install_result.stderr}")
|
| 214 |
+
|
| 215 |
+
except subprocess.TimeoutExpired:
|
| 216 |
+
result["errors"].append("Installation timeout")
|
| 217 |
+
except Exception as e:
|
| 218 |
+
result["errors"].append(f"Unexpected error: {str(e)}")
|
| 219 |
+
|
| 220 |
+
return result
|
| 221 |
+
|
| 222 |
+
def test_java_client(self) -> Dict[str, Any]:
|
| 223 |
+
"""Test Java client."""
|
| 224 |
+
client_dir = self.clients_dir / "java"
|
| 225 |
+
result = {
|
| 226 |
+
"language": "java",
|
| 227 |
+
"exists": False,
|
| 228 |
+
"compiles": False,
|
| 229 |
+
"has_examples": False,
|
| 230 |
+
"has_docs": False,
|
| 231 |
+
"errors": []
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
if not client_dir.exists():
|
| 236 |
+
result["errors"].append("Client directory does not exist")
|
| 237 |
+
return result
|
| 238 |
+
|
| 239 |
+
result["exists"] = True
|
| 240 |
+
|
| 241 |
+
# Check for essential files
|
| 242 |
+
essential_files = ["pom.xml", "README.md"]
|
| 243 |
+
for file in essential_files:
|
| 244 |
+
if not (client_dir / file).exists():
|
| 245 |
+
result["errors"].append(f"Missing {file}")
|
| 246 |
+
|
| 247 |
+
# Check for example file
|
| 248 |
+
if (client_dir / "example.java").exists():
|
| 249 |
+
result["has_examples"] = True
|
| 250 |
+
|
| 251 |
+
# Check for documentation
|
| 252 |
+
if (client_dir / "README.md").exists():
|
| 253 |
+
result["has_docs"] = True
|
| 254 |
+
|
| 255 |
+
# Try to compile the client (if Maven is available)
|
| 256 |
+
if (client_dir / "pom.xml").exists():
|
| 257 |
+
logger.info("Testing Java client compilation")
|
| 258 |
+
|
| 259 |
+
# Check if Maven is available
|
| 260 |
+
maven_check = subprocess.run(
|
| 261 |
+
["mvn", "--version"],
|
| 262 |
+
capture_output=True,
|
| 263 |
+
text=True,
|
| 264 |
+
timeout=10
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
if maven_check.returncode == 0:
|
| 268 |
+
compile_result = subprocess.run(
|
| 269 |
+
["mvn", "compile"],
|
| 270 |
+
cwd=client_dir,
|
| 271 |
+
capture_output=True,
|
| 272 |
+
text=True,
|
| 273 |
+
timeout=180
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
if compile_result.returncode == 0:
|
| 277 |
+
result["compiles"] = True
|
| 278 |
+
else:
|
| 279 |
+
result["errors"].append(f"Compilation failed: {compile_result.stderr}")
|
| 280 |
+
else:
|
| 281 |
+
result["errors"].append("Maven not available for compilation test")
|
| 282 |
+
|
| 283 |
+
except subprocess.TimeoutExpired:
|
| 284 |
+
result["errors"].append("Compilation timeout")
|
| 285 |
+
except Exception as e:
|
| 286 |
+
result["errors"].append(f"Unexpected error: {str(e)}")
|
| 287 |
+
|
| 288 |
+
return result
|
| 289 |
+
|
| 290 |
+
def test_client_structure(self, language: str) -> Dict[str, Any]:
|
| 291 |
+
"""Test the structure of a generated client."""
|
| 292 |
+
client_dir = self.clients_dir / language
|
| 293 |
+
result = {
|
| 294 |
+
"language": language,
|
| 295 |
+
"exists": False,
|
| 296 |
+
"structure_valid": False,
|
| 297 |
+
"has_api_files": False,
|
| 298 |
+
"has_model_files": False,
|
| 299 |
+
"has_docs": False,
|
| 300 |
+
"file_count": 0,
|
| 301 |
+
"errors": []
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
try:
|
| 305 |
+
if not client_dir.exists():
|
| 306 |
+
result["errors"].append("Client directory does not exist")
|
| 307 |
+
return result
|
| 308 |
+
|
| 309 |
+
result["exists"] = True
|
| 310 |
+
|
| 311 |
+
# Count files
|
| 312 |
+
all_files = list(client_dir.rglob("*"))
|
| 313 |
+
result["file_count"] = len([f for f in all_files if f.is_file()])
|
| 314 |
+
|
| 315 |
+
# Check for API files
|
| 316 |
+
api_files = list(client_dir.rglob("*api*"))
|
| 317 |
+
if api_files:
|
| 318 |
+
result["has_api_files"] = True
|
| 319 |
+
|
| 320 |
+
# Check for model files
|
| 321 |
+
model_files = list(client_dir.rglob("*model*"))
|
| 322 |
+
if model_files:
|
| 323 |
+
result["has_model_files"] = True
|
| 324 |
+
|
| 325 |
+
# Check for documentation
|
| 326 |
+
doc_files = list(client_dir.rglob("README*")) + list(client_dir.rglob("*.md"))
|
| 327 |
+
if doc_files:
|
| 328 |
+
result["has_docs"] = True
|
| 329 |
+
|
| 330 |
+
# Basic structure validation
|
| 331 |
+
if result["has_api_files"] and result["has_model_files"]:
|
| 332 |
+
result["structure_valid"] = True
|
| 333 |
+
|
| 334 |
+
except Exception as e:
|
| 335 |
+
result["errors"].append(f"Unexpected error: {str(e)}")
|
| 336 |
+
|
| 337 |
+
return result
|
| 338 |
+
|
| 339 |
+
def run_functional_tests(self) -> Dict[str, Any]:
|
| 340 |
+
"""Run functional tests against the API using generated clients."""
|
| 341 |
+
result = {
|
| 342 |
+
"api_reachable": False,
|
| 343 |
+
"openapi_valid": False,
|
| 344 |
+
"endpoints_tested": 0,
|
| 345 |
+
"endpoints_passed": 0,
|
| 346 |
+
"errors": []
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
try:
|
| 350 |
+
# Test API availability
|
| 351 |
+
result["api_reachable"] = self.check_api_availability()
|
| 352 |
+
|
| 353 |
+
# Test OpenAPI spec
|
| 354 |
+
result["openapi_valid"] = self.test_openapi_spec()
|
| 355 |
+
|
| 356 |
+
if not result["api_reachable"]:
|
| 357 |
+
result["errors"].append("API not reachable - skipping functional tests")
|
| 358 |
+
return result
|
| 359 |
+
|
| 360 |
+
# Test basic endpoints
|
| 361 |
+
endpoints_to_test = [
|
| 362 |
+
{"method": "GET", "path": "/health", "expected_status": 200},
|
| 363 |
+
{"method": "GET", "path": "/", "expected_status": 200},
|
| 364 |
+
{"method": "GET", "path": "/openapi.json", "expected_status": 200},
|
| 365 |
+
]
|
| 366 |
+
|
| 367 |
+
for endpoint in endpoints_to_test:
|
| 368 |
+
try:
|
| 369 |
+
result["endpoints_tested"] += 1
|
| 370 |
+
|
| 371 |
+
response = requests.request(
|
| 372 |
+
endpoint["method"],
|
| 373 |
+
f"{self.api_url}{endpoint['path']}",
|
| 374 |
+
timeout=10
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
if response.status_code == endpoint["expected_status"]:
|
| 378 |
+
result["endpoints_passed"] += 1
|
| 379 |
+
else:
|
| 380 |
+
result["errors"].append(
|
| 381 |
+
f"{endpoint['method']} {endpoint['path']} returned {response.status_code}, "
|
| 382 |
+
f"expected {endpoint['expected_status']}"
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
except requests.RequestException as e:
|
| 386 |
+
result["errors"].append(f"Failed to test {endpoint['path']}: {str(e)}")
|
| 387 |
+
|
| 388 |
+
except Exception as e:
|
| 389 |
+
result["errors"].append(f"Functional test error: {str(e)}")
|
| 390 |
+
|
| 391 |
+
return result
|
| 392 |
+
|
| 393 |
+
def run_all_tests(self) -> Dict[str, Any]:
|
| 394 |
+
"""Run all client tests."""
|
| 395 |
+
logger.info("Starting comprehensive client testing")
|
| 396 |
+
|
| 397 |
+
# Test API functionality
|
| 398 |
+
functional_results = self.run_functional_tests()
|
| 399 |
+
|
| 400 |
+
# Test individual clients
|
| 401 |
+
client_results = {}
|
| 402 |
+
|
| 403 |
+
# Test TypeScript client
|
| 404 |
+
if (self.clients_dir / "typescript").exists():
|
| 405 |
+
logger.info("Testing TypeScript client")
|
| 406 |
+
client_results["typescript"] = self.test_typescript_client()
|
| 407 |
+
|
| 408 |
+
# Test Python client
|
| 409 |
+
if (self.clients_dir / "python").exists():
|
| 410 |
+
logger.info("Testing Python client")
|
| 411 |
+
client_results["python"] = self.test_python_client()
|
| 412 |
+
|
| 413 |
+
# Test Java client
|
| 414 |
+
if (self.clients_dir / "java").exists():
|
| 415 |
+
logger.info("Testing Java client")
|
| 416 |
+
client_results["java"] = self.test_java_client()
|
| 417 |
+
|
| 418 |
+
# Test structure for all clients
|
| 419 |
+
structure_results = {}
|
| 420 |
+
for client_dir in self.clients_dir.iterdir():
|
| 421 |
+
if client_dir.is_dir():
|
| 422 |
+
language = client_dir.name
|
| 423 |
+
logger.info(f"Testing {language} client structure")
|
| 424 |
+
structure_results[language] = self.test_client_structure(language)
|
| 425 |
+
|
| 426 |
+
return {
|
| 427 |
+
"functional_tests": functional_results,
|
| 428 |
+
"client_tests": client_results,
|
| 429 |
+
"structure_tests": structure_results,
|
| 430 |
+
"summary": self._generate_summary(functional_results, client_results, structure_results)
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
def _generate_summary(self, functional: Dict, clients: Dict, structures: Dict) -> Dict[str, Any]:
|
| 434 |
+
"""Generate test summary."""
|
| 435 |
+
total_clients = len(structures)
|
| 436 |
+
working_clients = 0
|
| 437 |
+
|
| 438 |
+
for language, result in clients.items():
|
| 439 |
+
if language == "typescript" and result.get("builds", False):
|
| 440 |
+
working_clients += 1
|
| 441 |
+
elif language == "python" and result.get("imports", False):
|
| 442 |
+
working_clients += 1
|
| 443 |
+
elif language == "java" and result.get("compiles", False):
|
| 444 |
+
working_clients += 1
|
| 445 |
+
|
| 446 |
+
return {
|
| 447 |
+
"total_clients_found": total_clients,
|
| 448 |
+
"clients_tested": len(clients),
|
| 449 |
+
"working_clients": working_clients,
|
| 450 |
+
"api_functional": functional.get("api_reachable", False),
|
| 451 |
+
"openapi_valid": functional.get("openapi_valid", False),
|
| 452 |
+
"endpoints_success_rate": (
|
| 453 |
+
functional.get("endpoints_passed", 0) / max(functional.get("endpoints_tested", 1), 1) * 100
|
| 454 |
+
)
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
def print_results(self, results: Dict[str, Any]) -> None:
|
| 458 |
+
"""Print test results in a formatted way."""
|
| 459 |
+
print("\n" + "="*60)
|
| 460 |
+
print("CLIENT SDK TEST RESULTS")
|
| 461 |
+
print("="*60)
|
| 462 |
+
|
| 463 |
+
# Print summary
|
| 464 |
+
summary = results["summary"]
|
| 465 |
+
print(f"\nSUMMARY:")
|
| 466 |
+
print(f" Total clients found: {summary['total_clients_found']}")
|
| 467 |
+
print(f" Clients tested: {summary['clients_tested']}")
|
| 468 |
+
print(f" Working clients: {summary['working_clients']}")
|
| 469 |
+
print(f" API functional: {'✅' if summary['api_functional'] else '❌'}")
|
| 470 |
+
print(f" OpenAPI valid: {'✅' if summary['openapi_valid'] else '❌'}")
|
| 471 |
+
print(f" Endpoints success rate: {summary['endpoints_success_rate']:.1f}%")
|
| 472 |
+
|
| 473 |
+
# Print functional test results
|
| 474 |
+
print(f"\nFUNCTIONAL TESTS:")
|
| 475 |
+
functional = results["functional_tests"]
|
| 476 |
+
print(f" API reachable: {'✅' if functional['api_reachable'] else '❌'}")
|
| 477 |
+
print(f" OpenAPI valid: {'✅' if functional['openapi_valid'] else '❌'}")
|
| 478 |
+
print(f" Endpoints tested: {functional['endpoints_tested']}")
|
| 479 |
+
print(f" Endpoints passed: {functional['endpoints_passed']}")
|
| 480 |
+
|
| 481 |
+
if functional["errors"]:
|
| 482 |
+
print(" Errors:")
|
| 483 |
+
for error in functional["errors"]:
|
| 484 |
+
print(f" - {error}")
|
| 485 |
+
|
| 486 |
+
# Print client test results
|
| 487 |
+
print(f"\nCLIENT TESTS:")
|
| 488 |
+
for language, result in results["client_tests"].items():
|
| 489 |
+
print(f"\n {language.upper()}:")
|
| 490 |
+
print(f" Exists: {'✅' if result['exists'] else '❌'}")
|
| 491 |
+
|
| 492 |
+
if language == "typescript":
|
| 493 |
+
print(f" Builds: {'✅' if result['builds'] else '❌'}")
|
| 494 |
+
elif language == "python":
|
| 495 |
+
print(f" Installs: {'✅' if result['installs'] else '❌'}")
|
| 496 |
+
print(f" Imports: {'✅' if result['imports'] else '❌'}")
|
| 497 |
+
elif language == "java":
|
| 498 |
+
print(f" Compiles: {'✅' if result['compiles'] else '❌'}")
|
| 499 |
+
|
| 500 |
+
print(f" Has examples: {'✅' if result['has_examples'] else '❌'}")
|
| 501 |
+
print(f" Has docs: {'✅' if result['has_docs'] else '❌'}")
|
| 502 |
+
|
| 503 |
+
if result["errors"]:
|
| 504 |
+
print(" Errors:")
|
| 505 |
+
for error in result["errors"]:
|
| 506 |
+
print(f" - {error}")
|
| 507 |
+
|
| 508 |
+
# Print structure test results
|
| 509 |
+
print(f"\nSTRUCTURE TESTS:")
|
| 510 |
+
for language, result in results["structure_tests"].items():
|
| 511 |
+
print(f"\n {language.upper()}:")
|
| 512 |
+
print(f" Exists: {'✅' if result['exists'] else '❌'}")
|
| 513 |
+
print(f" Structure valid: {'✅' if result['structure_valid'] else '❌'}")
|
| 514 |
+
print(f" Has API files: {'✅' if result['has_api_files'] else '❌'}")
|
| 515 |
+
print(f" Has model files: {'✅' if result['has_model_files'] else '❌'}")
|
| 516 |
+
print(f" Has docs: {'✅' if result['has_docs'] else '❌'}")
|
| 517 |
+
print(f" File count: {result['file_count']}")
|
| 518 |
+
|
| 519 |
+
if result["errors"]:
|
| 520 |
+
print(" Errors:")
|
| 521 |
+
for error in result["errors"]:
|
| 522 |
+
print(f" - {error}")
|
| 523 |
+
|
| 524 |
+
print("\n" + "="*60)
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def main():
|
| 528 |
+
"""Main function for command-line usage."""
|
| 529 |
+
import argparse
|
| 530 |
+
|
| 531 |
+
parser = argparse.ArgumentParser(description="Test generated client SDKs")
|
| 532 |
+
parser.add_argument(
|
| 533 |
+
"--api-url",
|
| 534 |
+
default="http://localhost:8000",
|
| 535 |
+
help="Base URL of the API (default: http://localhost:8000)"
|
| 536 |
+
)
|
| 537 |
+
parser.add_argument(
|
| 538 |
+
"--clients-dir",
|
| 539 |
+
default="generated_clients",
|
| 540 |
+
help="Directory containing generated clients (default: generated_clients)"
|
| 541 |
+
)
|
| 542 |
+
parser.add_argument(
|
| 543 |
+
"--output-file",
|
| 544 |
+
help="Save test results to JSON file"
|
| 545 |
+
)
|
| 546 |
+
parser.add_argument(
|
| 547 |
+
"--verbose",
|
| 548 |
+
action="store_true",
|
| 549 |
+
help="Enable verbose logging"
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
args = parser.parse_args()
|
| 553 |
+
|
| 554 |
+
if args.verbose:
|
| 555 |
+
logging.getLogger().setLevel(logging.DEBUG)
|
| 556 |
+
|
| 557 |
+
# Create tester instance
|
| 558 |
+
tester = ClientTester(args.api_url, args.clients_dir)
|
| 559 |
+
|
| 560 |
+
# Run tests
|
| 561 |
+
results = tester.run_all_tests()
|
| 562 |
+
|
| 563 |
+
# Print results
|
| 564 |
+
tester.print_results(results)
|
| 565 |
+
|
| 566 |
+
# Save results to file if requested
|
| 567 |
+
if args.output_file:
|
| 568 |
+
with open(args.output_file, 'w') as f:
|
| 569 |
+
json.dump(results, f, indent=2, default=str)
|
| 570 |
+
logger.info(f"Test results saved to {args.output_file}")
|
| 571 |
+
|
| 572 |
+
# Exit with error code if tests failed
|
| 573 |
+
summary = results["summary"]
|
| 574 |
+
if not summary["api_functional"] or summary["working_clients"] == 0:
|
| 575 |
+
sys.exit(1)
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
if __name__ == "__main__":
|
| 579 |
+
main()
|
setup-dev.bat
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
REM T2M FastAPI Development Setup Script for Windows
|
| 3 |
+
REM This script sets up Redis, ngrok, and provides instructions for FastAPI
|
| 4 |
+
|
| 5 |
+
echo 🚀 Setting up T2M FastAPI Development Environment
|
| 6 |
+
echo ================================================
|
| 7 |
+
|
| 8 |
+
REM Check if Docker is installed
|
| 9 |
+
docker --version >nul 2>&1
|
| 10 |
+
if %errorlevel% neq 0 (
|
| 11 |
+
echo ❌ Docker is not installed. Please install Docker Desktop first.
|
| 12 |
+
pause
|
| 13 |
+
exit /b 1
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
REM Check if Docker Compose is installed
|
| 17 |
+
docker-compose --version >nul 2>&1
|
| 18 |
+
if %errorlevel% neq 0 (
|
| 19 |
+
echo ❌ Docker Compose is not installed. Please install Docker Compose first.
|
| 20 |
+
pause
|
| 21 |
+
exit /b 1
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
REM Check if .env file exists
|
| 25 |
+
if not exist ".env" (
|
| 26 |
+
echo ❌ .env file not found. Please copy .env.example to .env and configure it.
|
| 27 |
+
pause
|
| 28 |
+
exit /b 1
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
REM Check if NGROK_AUTHTOKEN is set
|
| 32 |
+
findstr /C:"NGROK_AUTHTOKEN=" .env >nul
|
| 33 |
+
if %errorlevel% neq 0 (
|
| 34 |
+
echo ❌ NGROK_AUTHTOKEN is not set in .env file.
|
| 35 |
+
echo Please get your auth token from https://dashboard.ngrok.com/get-started/your-authtoken
|
| 36 |
+
echo and add it to your .env file: NGROK_AUTHTOKEN=your_token_here
|
| 37 |
+
pause
|
| 38 |
+
exit /b 1
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
echo ✅ Prerequisites check passed
|
| 42 |
+
echo.
|
| 43 |
+
|
| 44 |
+
REM Start Redis and ngrok services
|
| 45 |
+
echo 🔧 Starting Redis and ngrok services...
|
| 46 |
+
docker-compose -f docker-compose.dev.yml up -d
|
| 47 |
+
|
| 48 |
+
echo.
|
| 49 |
+
echo ⏳ Waiting for services to start...
|
| 50 |
+
timeout /t 5 /nobreak >nul
|
| 51 |
+
|
| 52 |
+
REM Check if services are running
|
| 53 |
+
docker-compose -f docker-compose.dev.yml ps
|
| 54 |
+
|
| 55 |
+
echo.
|
| 56 |
+
echo 🎯 Next Steps:
|
| 57 |
+
echo ==============
|
| 58 |
+
echo.
|
| 59 |
+
echo 1. Start your FastAPI server locally:
|
| 60 |
+
echo python -m uvicorn src.app.main:app --reload --host 0.0.0.0 --port 8000
|
| 61 |
+
echo.
|
| 62 |
+
echo 2. Get your public ngrok URL:
|
| 63 |
+
echo - Visit: http://localhost:4040
|
| 64 |
+
echo - Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
|
| 65 |
+
echo.
|
| 66 |
+
echo 3. Configure Clerk webhook:
|
| 67 |
+
echo - Go to https://dashboard.clerk.com/
|
| 68 |
+
echo - Navigate to Webhooks
|
| 69 |
+
echo - Add endpoint: https://your-ngrok-url.ngrok.io/api/v1/auth/webhooks/clerk
|
| 70 |
+
echo - Copy the signing secret to CLERK_WEBHOOK_SECRET in .env
|
| 71 |
+
echo.
|
| 72 |
+
echo 4. Test your setup:
|
| 73 |
+
echo curl https://your-ngrok-url.ngrok.io/health
|
| 74 |
+
echo.
|
| 75 |
+
echo 📝 Useful Commands:
|
| 76 |
+
echo ==================
|
| 77 |
+
echo • View logs: docker-compose -f docker-compose.dev.yml logs -f
|
| 78 |
+
echo • Stop services: docker-compose -f docker-compose.dev.yml down
|
| 79 |
+
echo • Restart services: docker-compose -f docker-compose.dev.yml restart
|
| 80 |
+
echo • Check Redis: redis-cli ping
|
| 81 |
+
echo.
|
| 82 |
+
echo 🌐 URLs:
|
| 83 |
+
echo ========
|
| 84 |
+
echo • FastAPI (local): http://localhost:8000
|
| 85 |
+
echo • FastAPI Docs: http://localhost:8000/docs
|
| 86 |
+
echo • ngrok Dashboard: http://localhost:4040
|
| 87 |
+
echo • Redis: localhost:6379
|
| 88 |
+
echo.
|
| 89 |
+
echo ✨ Development environment is ready!
|
| 90 |
+
pause
|
setup-dev.sh
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# T2M FastAPI Development Setup Script
|
| 4 |
+
# This script sets up Redis, ngrok, and provides instructions for FastAPI
|
| 5 |
+
|
| 6 |
+
set -e
|
| 7 |
+
|
| 8 |
+
echo "🚀 Setting up T2M FastAPI Development Environment"
|
| 9 |
+
echo "================================================"
|
| 10 |
+
|
| 11 |
+
# Check if Docker is installed
|
| 12 |
+
if ! command -v docker &> /dev/null; then
|
| 13 |
+
echo "❌ Docker is not installed. Please install Docker first."
|
| 14 |
+
exit 1
|
| 15 |
+
fi
|
| 16 |
+
|
| 17 |
+
# Check if Docker Compose is installed
|
| 18 |
+
if ! command -v docker-compose &> /dev/null; then
|
| 19 |
+
echo "❌ Docker Compose is not installed. Please install Docker Compose first."
|
| 20 |
+
exit 1
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# Check if .env file exists
|
| 24 |
+
if [ ! -f ".env" ]; then
|
| 25 |
+
echo "❌ .env file not found. Please copy .env.example to .env and configure it."
|
| 26 |
+
exit 1
|
| 27 |
+
fi
|
| 28 |
+
|
| 29 |
+
# Check if NGROK_AUTHTOKEN is set
|
| 30 |
+
if ! grep -q "NGROK_AUTHTOKEN=" .env || grep -q "NGROK_AUTHTOKEN=$" .env; then
|
| 31 |
+
echo "❌ NGROK_AUTHTOKEN is not set in .env file."
|
| 32 |
+
echo " Please get your auth token from https://dashboard.ngrok.com/get-started/your-authtoken"
|
| 33 |
+
echo " and add it to your .env file: NGROK_AUTHTOKEN=your_token_here"
|
| 34 |
+
exit 1
|
| 35 |
+
fi
|
| 36 |
+
|
| 37 |
+
echo "✅ Prerequisites check passed"
|
| 38 |
+
echo ""
|
| 39 |
+
|
| 40 |
+
# Start Redis and ngrok services
|
| 41 |
+
echo "🔧 Starting Redis and ngrok services..."
|
| 42 |
+
|
| 43 |
+
# Try the simple compose file first
|
| 44 |
+
if docker-compose -f docker-compose.simple.yml up -d; then
|
| 45 |
+
echo "✅ Using simple Docker Compose configuration"
|
| 46 |
+
COMPOSE_FILE="docker-compose.simple.yml"
|
| 47 |
+
else
|
| 48 |
+
echo "⚠️ Falling back to development configuration"
|
| 49 |
+
docker-compose -f docker-compose.dev.yml up -d
|
| 50 |
+
COMPOSE_FILE="docker-compose.dev.yml"
|
| 51 |
+
fi
|
| 52 |
+
|
| 53 |
+
echo ""
|
| 54 |
+
echo "⏳ Waiting for services to start..."
|
| 55 |
+
sleep 5
|
| 56 |
+
|
| 57 |
+
# Check if Redis is running
|
| 58 |
+
if docker-compose -f $COMPOSE_FILE ps redis | grep -q "Up"; then
|
| 59 |
+
echo "✅ Redis is running on localhost:6379"
|
| 60 |
+
else
|
| 61 |
+
echo "❌ Redis failed to start"
|
| 62 |
+
exit 1
|
| 63 |
+
fi
|
| 64 |
+
|
| 65 |
+
# Check if ngrok is running
|
| 66 |
+
if docker-compose -f $COMPOSE_FILE ps ngrok | grep -q "Up"; then
|
| 67 |
+
echo "✅ ngrok is running"
|
| 68 |
+
echo " 📊 ngrok Web Interface: http://localhost:4040"
|
| 69 |
+
else
|
| 70 |
+
echo "❌ ngrok failed to start"
|
| 71 |
+
exit 1
|
| 72 |
+
fi
|
| 73 |
+
|
| 74 |
+
echo ""
|
| 75 |
+
echo "🎯 Next Steps:"
|
| 76 |
+
echo "=============="
|
| 77 |
+
echo ""
|
| 78 |
+
echo "1. Start your FastAPI server locally:"
|
| 79 |
+
echo " python -m uvicorn src.app.main:app --reload --host 0.0.0.0 --port 8000"
|
| 80 |
+
echo ""
|
| 81 |
+
echo "2. Get your public ngrok URL:"
|
| 82 |
+
echo " - Visit: http://localhost:4040"
|
| 83 |
+
echo " - Copy the HTTPS URL (e.g., https://abc123.ngrok.io)"
|
| 84 |
+
echo ""
|
| 85 |
+
echo "3. Configure Clerk webhook:"
|
| 86 |
+
echo " - Go to https://dashboard.clerk.com/"
|
| 87 |
+
echo " - Navigate to Webhooks"
|
| 88 |
+
echo " - Add endpoint: https://your-ngrok-url.ngrok.io/api/v1/auth/webhooks/clerk"
|
| 89 |
+
echo " - Copy the signing secret to CLERK_WEBHOOK_SECRET in .env"
|
| 90 |
+
echo ""
|
| 91 |
+
echo "4. Test your setup:"
|
| 92 |
+
echo " curl https://your-ngrok-url.ngrok.io/health"
|
| 93 |
+
echo ""
|
| 94 |
+
echo "📝 Useful Commands:"
|
| 95 |
+
echo "=================="
|
| 96 |
+
echo "• View logs: docker-compose -f $COMPOSE_FILE logs -f"
|
| 97 |
+
echo "• Stop services: docker-compose -f $COMPOSE_FILE down"
|
| 98 |
+
echo "• Restart services: docker-compose -f $COMPOSE_FILE restart"
|
| 99 |
+
echo "• Check Redis: redis-cli ping"
|
| 100 |
+
echo ""
|
| 101 |
+
echo "🌐 URLs:"
|
| 102 |
+
echo "========"
|
| 103 |
+
echo "• FastAPI (local): http://localhost:8000"
|
| 104 |
+
echo "• FastAPI Docs: http://localhost:8000/docs"
|
| 105 |
+
echo "• ngrok Dashboard: http://localhost:4040"
|
| 106 |
+
echo "• Redis: localhost:6379"
|
| 107 |
+
echo ""
|
| 108 |
+
echo "✨ Development environment is ready!"
|
simple_app.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from simple_config import SimpleSettings
|
| 6 |
+
|
| 7 |
+
# Create settings
|
| 8 |
+
settings = SimpleSettings()
|
| 9 |
+
|
| 10 |
+
# Create FastAPI app
|
| 11 |
+
app = FastAPI(
|
| 12 |
+
title=settings.app_name,
|
| 13 |
+
debug=settings.debug,
|
| 14 |
+
description="A simple FastAPI application for testing",
|
| 15 |
+
version="1.0.0",
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Pydantic models for API documentation
|
| 19 |
+
class MessageRequest(BaseModel):
|
| 20 |
+
message: str
|
| 21 |
+
user_id: str = None
|
| 22 |
+
|
| 23 |
+
class MessageResponse(BaseModel):
|
| 24 |
+
response: str
|
| 25 |
+
timestamp: str
|
| 26 |
+
|
| 27 |
+
@app.get("/")
|
| 28 |
+
async def root():
|
| 29 |
+
return {
|
| 30 |
+
"message": f"Welcome to {settings.app_name}",
|
| 31 |
+
"status": "running"
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
@app.get("/health")
|
| 35 |
+
async def health():
|
| 36 |
+
return {
|
| 37 |
+
"status": "healthy",
|
| 38 |
+
"app": settings.app_name,
|
| 39 |
+
"redis_host": settings.redis_host,
|
| 40 |
+
"redis_port": settings.redis_port,
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
@app.get("/config")
|
| 44 |
+
async def config():
|
| 45 |
+
return {
|
| 46 |
+
"origins": settings.get_allowed_origins(),
|
| 47 |
+
"methods": settings.get_allowed_methods(),
|
| 48 |
+
"clerk_configured": bool(settings.clerk_secret_key),
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
@app.post("/api/message", response_model=MessageResponse)
|
| 52 |
+
async def send_message(request: MessageRequest):
|
| 53 |
+
"""Send a message and get a response."""
|
| 54 |
+
from datetime import datetime
|
| 55 |
+
return MessageResponse(
|
| 56 |
+
response=f"Received: {request.message}",
|
| 57 |
+
timestamp=datetime.now().isoformat()
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
@app.get("/api/users/{user_id}")
|
| 61 |
+
async def get_user(user_id: str):
|
| 62 |
+
"""Get user information by ID."""
|
| 63 |
+
return {
|
| 64 |
+
"user_id": user_id,
|
| 65 |
+
"name": f"User {user_id}",
|
| 66 |
+
"status": "active"
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
import uvicorn
|
| 71 |
+
uvicorn.run(
|
| 72 |
+
"simple_app:app",
|
| 73 |
+
host=settings.host,
|
| 74 |
+
port=settings.port,
|
| 75 |
+
reload=True,
|
| 76 |
+
)
|
simple_config.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
from typing import List
|
| 4 |
+
from pydantic import Field
|
| 5 |
+
from pydantic_settings import BaseSettings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class SimpleSettings(BaseSettings):
|
| 9 |
+
"""Simple settings for testing."""
|
| 10 |
+
|
| 11 |
+
# Application settings
|
| 12 |
+
app_name: str = Field(default="FastAPI Video Backend", env="APP_NAME")
|
| 13 |
+
debug: bool = Field(default=False, env="DEBUG")
|
| 14 |
+
host: str = Field(default="0.0.0.0", env="HOST")
|
| 15 |
+
port: int = Field(default=8000, env="PORT")
|
| 16 |
+
|
| 17 |
+
# CORS settings as strings
|
| 18 |
+
allowed_origins: str = Field(
|
| 19 |
+
default="http://localhost:3000,http://localhost:8080",
|
| 20 |
+
env="ALLOWED_ORIGINS"
|
| 21 |
+
)
|
| 22 |
+
allowed_methods: str = Field(
|
| 23 |
+
default="GET,POST,PUT,DELETE,OPTIONS",
|
| 24 |
+
env="ALLOWED_METHODS"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Redis settings
|
| 28 |
+
redis_host: str = Field(default="localhost", env="REDIS_HOST")
|
| 29 |
+
redis_port: int = Field(default=6379, env="REDIS_PORT")
|
| 30 |
+
|
| 31 |
+
# Clerk settings
|
| 32 |
+
clerk_secret_key: str = Field(default="", env="CLERK_SECRET_KEY")
|
| 33 |
+
clerk_publishable_key: str = Field(default="", env="CLERK_PUBLISHABLE_KEY")
|
| 34 |
+
|
| 35 |
+
def get_allowed_origins(self) -> List[str]:
|
| 36 |
+
return [origin.strip() for origin in self.allowed_origins.split(",")]
|
| 37 |
+
|
| 38 |
+
def get_allowed_methods(self) -> List[str]:
|
| 39 |
+
return [method.strip() for method in self.allowed_methods.split(",")]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
settings = SimpleSettings()
|
| 44 |
+
print("✅ Simple config loaded successfully!")
|
| 45 |
+
print(f"App: {settings.app_name}")
|
| 46 |
+
print(f"Origins: {settings.get_allowed_origins()}")
|
| 47 |
+
print(f"Methods: {settings.get_allowed_methods()}")
|
| 48 |
+
print(f"Clerk Secret: {settings.clerk_secret_key[:20]}..." if settings.clerk_secret_key else "No Clerk secret")
|
src/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# FastAPI Video Generation Backend
|
src/app/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# API layer
|
src/app/api/dependencies.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI dependencies for authentication, services, and common utilities.
|
| 3 |
+
|
| 4 |
+
This module provides reusable dependencies for FastAPI endpoints,
|
| 5 |
+
including authentication, service injection, and request validation.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Dict, Any, Optional
|
| 10 |
+
from fastapi import Depends, HTTPException, status, Request, Header
|
| 11 |
+
from redis.asyncio import Redis
|
| 12 |
+
|
| 13 |
+
from ..core.auth import verify_clerk_token, AuthenticationError, extract_bearer_token
|
| 14 |
+
from ..core.redis import get_redis
|
| 15 |
+
from ..services.video_service import VideoService
|
| 16 |
+
from ..services.job_service import JobService
|
| 17 |
+
from ..services.queue_service import QueueService
|
| 18 |
+
from ..services.file_service import FileService
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
async def get_current_user(
|
| 24 |
+
authorization: Optional[str] = Header(None),
|
| 25 |
+
redis_client: Redis = Depends(get_redis)
|
| 26 |
+
) -> Dict[str, Any]:
|
| 27 |
+
"""
|
| 28 |
+
FastAPI dependency to get current authenticated user.
|
| 29 |
+
|
| 30 |
+
Extracts and validates the Clerk session token from the Authorization header,
|
| 31 |
+
then returns user information and token claims.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
authorization: Authorization header with Bearer token
|
| 35 |
+
redis_client: Redis client for caching user info
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
Dict containing user information and token claims
|
| 39 |
+
|
| 40 |
+
Raises:
|
| 41 |
+
HTTPException: If authentication fails
|
| 42 |
+
"""
|
| 43 |
+
try:
|
| 44 |
+
if not authorization:
|
| 45 |
+
raise AuthenticationError("Missing authorization header")
|
| 46 |
+
|
| 47 |
+
# Extract bearer token
|
| 48 |
+
token = extract_bearer_token(authorization)
|
| 49 |
+
|
| 50 |
+
# Verify token and get user info
|
| 51 |
+
auth_info = await verify_clerk_token(token)
|
| 52 |
+
|
| 53 |
+
# Extract email safely for logging
|
| 54 |
+
user_info = auth_info["user_info"]
|
| 55 |
+
user_id = user_info.get("id", "unknown")
|
| 56 |
+
|
| 57 |
+
# Extract email from email_addresses array if available
|
| 58 |
+
email = user_info.get("email")
|
| 59 |
+
if not email and "email_addresses" in user_info:
|
| 60 |
+
email_addresses = user_info.get("email_addresses", [])
|
| 61 |
+
if email_addresses:
|
| 62 |
+
primary_email_id = user_info.get("primary_email_address_id")
|
| 63 |
+
if primary_email_id:
|
| 64 |
+
primary_email = next((e for e in email_addresses if e.get("id") == primary_email_id), None)
|
| 65 |
+
if primary_email:
|
| 66 |
+
email = primary_email.get("email_address")
|
| 67 |
+
# Fallback to first email
|
| 68 |
+
if not email:
|
| 69 |
+
email = email_addresses[0].get("email_address")
|
| 70 |
+
|
| 71 |
+
logger.debug(
|
| 72 |
+
"User authenticated successfully",
|
| 73 |
+
user_id=user_id,
|
| 74 |
+
email=email or "no-email"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
return auth_info
|
| 78 |
+
|
| 79 |
+
except AuthenticationError as e:
|
| 80 |
+
logger.warning(f"Authentication failed: {e}")
|
| 81 |
+
raise HTTPException(
|
| 82 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 83 |
+
detail=str(e),
|
| 84 |
+
headers={"WWW-Authenticate": "Bearer"}
|
| 85 |
+
)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
logger.error(f"Authentication error: {e}", exc_info=True)
|
| 88 |
+
raise HTTPException(
|
| 89 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 90 |
+
detail="Authentication service error"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
async def get_optional_user(
|
| 95 |
+
authorization: Optional[str] = Header(None),
|
| 96 |
+
redis_client: Redis = Depends(get_redis)
|
| 97 |
+
) -> Optional[Dict[str, Any]]:
|
| 98 |
+
"""
|
| 99 |
+
FastAPI dependency to get current user if authenticated (optional).
|
| 100 |
+
|
| 101 |
+
Similar to get_current_user but returns None if no authentication
|
| 102 |
+
is provided instead of raising an exception.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
authorization: Authorization header with Bearer token (optional)
|
| 106 |
+
redis_client: Redis client for caching user info
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Dict containing user information or None if not authenticated
|
| 110 |
+
"""
|
| 111 |
+
try:
|
| 112 |
+
if not authorization:
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
return await get_current_user(authorization, redis_client)
|
| 116 |
+
|
| 117 |
+
except HTTPException:
|
| 118 |
+
# Return None for optional authentication
|
| 119 |
+
return None
|
| 120 |
+
except Exception as e:
|
| 121 |
+
logger.error(f"Optional authentication error: {e}", exc_info=True)
|
| 122 |
+
return None
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def get_video_service(
|
| 126 |
+
redis_client: Redis = Depends(get_redis)
|
| 127 |
+
) -> VideoService:
|
| 128 |
+
"""
|
| 129 |
+
FastAPI dependency to get VideoService instance.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
redis_client: Redis client dependency
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
VideoService instance
|
| 136 |
+
"""
|
| 137 |
+
return VideoService(redis_client)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def get_job_service(
|
| 141 |
+
redis_client: Redis = Depends(get_redis)
|
| 142 |
+
) -> JobService:
|
| 143 |
+
"""
|
| 144 |
+
FastAPI dependency to get JobService instance.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
redis_client: Redis client dependency
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
JobService instance
|
| 151 |
+
"""
|
| 152 |
+
return JobService(redis_client)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def get_queue_service(
|
| 156 |
+
redis_client: Redis = Depends(get_redis)
|
| 157 |
+
) -> QueueService:
|
| 158 |
+
"""
|
| 159 |
+
FastAPI dependency to get QueueService instance.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
redis_client: Redis client dependency
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
QueueService instance
|
| 166 |
+
"""
|
| 167 |
+
return QueueService(redis_client)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def get_file_service(
|
| 171 |
+
redis_client: Redis = Depends(get_redis)
|
| 172 |
+
) -> FileService:
|
| 173 |
+
"""
|
| 174 |
+
FastAPI dependency to get FileService instance.
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
redis_client: Redis client dependency
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
FileService instance
|
| 181 |
+
"""
|
| 182 |
+
return FileService(redis_client)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class PaginationParams:
|
| 186 |
+
"""
|
| 187 |
+
Pagination parameters for list endpoints.
|
| 188 |
+
"""
|
| 189 |
+
|
| 190 |
+
def __init__(
|
| 191 |
+
self,
|
| 192 |
+
page: int = 1,
|
| 193 |
+
items_per_page: int = 10,
|
| 194 |
+
max_items_per_page: int = 100
|
| 195 |
+
):
|
| 196 |
+
# Validate page number
|
| 197 |
+
if page < 1:
|
| 198 |
+
raise HTTPException(
|
| 199 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 200 |
+
detail="Page number must be greater than 0"
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
# Validate items per page
|
| 204 |
+
if items_per_page < 1:
|
| 205 |
+
raise HTTPException(
|
| 206 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 207 |
+
detail="Items per page must be greater than 0"
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
if items_per_page > max_items_per_page:
|
| 211 |
+
raise HTTPException(
|
| 212 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 213 |
+
detail=f"Items per page cannot exceed {max_items_per_page}"
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
self.page = page
|
| 217 |
+
self.items_per_page = items_per_page
|
| 218 |
+
self.offset = (page - 1) * items_per_page
|
| 219 |
+
|
| 220 |
+
@property
|
| 221 |
+
def limit(self) -> int:
|
| 222 |
+
"""Get limit for database queries."""
|
| 223 |
+
return self.items_per_page
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def get_pagination_params(
|
| 227 |
+
page: int = 1,
|
| 228 |
+
items_per_page: int = 10
|
| 229 |
+
) -> PaginationParams:
|
| 230 |
+
"""
|
| 231 |
+
FastAPI dependency to get pagination parameters.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
page: Page number (1-based)
|
| 235 |
+
items_per_page: Number of items per page
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
PaginationParams instance
|
| 239 |
+
|
| 240 |
+
Raises:
|
| 241 |
+
HTTPException: If parameters are invalid
|
| 242 |
+
"""
|
| 243 |
+
return PaginationParams(page=page, items_per_page=items_per_page)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
class JobFilters:
|
| 247 |
+
"""
|
| 248 |
+
Filtering parameters for job list endpoints.
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
def __init__(
|
| 252 |
+
self,
|
| 253 |
+
status: Optional[str] = None,
|
| 254 |
+
job_type: Optional[str] = None,
|
| 255 |
+
priority: Optional[str] = None,
|
| 256 |
+
created_after: Optional[str] = None,
|
| 257 |
+
created_before: Optional[str] = None
|
| 258 |
+
):
|
| 259 |
+
self.status = status
|
| 260 |
+
self.job_type = job_type
|
| 261 |
+
self.priority = priority
|
| 262 |
+
self.created_after = created_after
|
| 263 |
+
self.created_before = created_before
|
| 264 |
+
|
| 265 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 266 |
+
"""Convert filters to dictionary for service layer."""
|
| 267 |
+
return {
|
| 268 |
+
k: v for k, v in {
|
| 269 |
+
"status": self.status,
|
| 270 |
+
"job_type": self.job_type,
|
| 271 |
+
"priority": self.priority,
|
| 272 |
+
"created_after": self.created_after,
|
| 273 |
+
"created_before": self.created_before
|
| 274 |
+
}.items() if v is not None
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def get_job_filters(
|
| 279 |
+
status: Optional[str] = None,
|
| 280 |
+
job_type: Optional[str] = None,
|
| 281 |
+
priority: Optional[str] = None,
|
| 282 |
+
created_after: Optional[str] = None,
|
| 283 |
+
created_before: Optional[str] = None
|
| 284 |
+
) -> JobFilters:
|
| 285 |
+
"""
|
| 286 |
+
FastAPI dependency to get job filtering parameters.
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
status: Filter by job status
|
| 290 |
+
job_type: Filter by job type
|
| 291 |
+
priority: Filter by job priority
|
| 292 |
+
created_after: Filter jobs created after this date (ISO format)
|
| 293 |
+
created_before: Filter jobs created before this date (ISO format)
|
| 294 |
+
|
| 295 |
+
Returns:
|
| 296 |
+
JobFilters instance
|
| 297 |
+
"""
|
| 298 |
+
return JobFilters(
|
| 299 |
+
status=status,
|
| 300 |
+
job_type=job_type,
|
| 301 |
+
priority=priority,
|
| 302 |
+
created_after=created_after,
|
| 303 |
+
created_before=created_before
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def validate_job_ownership(
|
| 308 |
+
job_user_id: str,
|
| 309 |
+
current_user: Dict[str, Any]
|
| 310 |
+
) -> None:
|
| 311 |
+
"""
|
| 312 |
+
Validate that the current user owns the specified job.
|
| 313 |
+
|
| 314 |
+
Args:
|
| 315 |
+
job_user_id: User ID from the job
|
| 316 |
+
current_user: Current authenticated user
|
| 317 |
+
|
| 318 |
+
Raises:
|
| 319 |
+
HTTPException: If user doesn't own the job
|
| 320 |
+
"""
|
| 321 |
+
if job_user_id != current_user["user_info"]["id"]:
|
| 322 |
+
raise HTTPException(
|
| 323 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 324 |
+
detail="Access denied: You don't own this resource"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def validate_request_size(
|
| 329 |
+
request: Request,
|
| 330 |
+
max_size_mb: int = 10
|
| 331 |
+
) -> None:
|
| 332 |
+
"""
|
| 333 |
+
Validate request content size.
|
| 334 |
+
|
| 335 |
+
Args:
|
| 336 |
+
request: FastAPI request object
|
| 337 |
+
max_size_mb: Maximum allowed size in MB
|
| 338 |
+
|
| 339 |
+
Raises:
|
| 340 |
+
HTTPException: If request is too large
|
| 341 |
+
"""
|
| 342 |
+
content_length = request.headers.get("content-length")
|
| 343 |
+
if content_length:
|
| 344 |
+
size_mb = int(content_length) / (1024 * 1024)
|
| 345 |
+
if size_mb > max_size_mb:
|
| 346 |
+
raise HTTPException(
|
| 347 |
+
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
| 348 |
+
detail=f"Request too large. Maximum size: {max_size_mb}MB"
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
async def rate_limit_check(
|
| 353 |
+
current_user: Dict[str, Any],
|
| 354 |
+
redis_client: Redis,
|
| 355 |
+
operation: str,
|
| 356 |
+
limit: int = 10,
|
| 357 |
+
window_seconds: int = 60
|
| 358 |
+
) -> None:
|
| 359 |
+
"""
|
| 360 |
+
Check rate limits for user operations.
|
| 361 |
+
|
| 362 |
+
Args:
|
| 363 |
+
current_user: Current authenticated user
|
| 364 |
+
redis_client: Redis client for rate limit storage
|
| 365 |
+
operation: Operation name for rate limiting
|
| 366 |
+
limit: Maximum operations per window
|
| 367 |
+
window_seconds: Time window in seconds
|
| 368 |
+
|
| 369 |
+
Raises:
|
| 370 |
+
HTTPException: If rate limit exceeded
|
| 371 |
+
"""
|
| 372 |
+
user_id = current_user["user_info"]["id"]
|
| 373 |
+
key = f"rate_limit:{user_id}:{operation}"
|
| 374 |
+
|
| 375 |
+
try:
|
| 376 |
+
# Get current count
|
| 377 |
+
current_count = await redis_client.get(key)
|
| 378 |
+
current_count = int(current_count) if current_count else 0
|
| 379 |
+
|
| 380 |
+
if current_count >= limit:
|
| 381 |
+
raise HTTPException(
|
| 382 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 383 |
+
detail=f"Rate limit exceeded for {operation}. Try again later.",
|
| 384 |
+
headers={"Retry-After": str(window_seconds)}
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
# Increment counter
|
| 388 |
+
pipe = redis_client.pipeline()
|
| 389 |
+
pipe.incr(key)
|
| 390 |
+
pipe.expire(key, window_seconds)
|
| 391 |
+
await pipe.execute()
|
| 392 |
+
|
| 393 |
+
except HTTPException:
|
| 394 |
+
raise
|
| 395 |
+
except Exception as e:
|
| 396 |
+
logger.error(f"Rate limit check failed: {e}", exc_info=True)
|
| 397 |
+
# Don't block requests if rate limiting fails
|
| 398 |
+
pass
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
# Additional authentication dependencies for compatibility
|
| 402 |
+
async def get_authenticated_user(
|
| 403 |
+
authorization: Optional[str] = Header(None),
|
| 404 |
+
redis_client: Redis = Depends(get_redis)
|
| 405 |
+
) -> Dict[str, Any]:
|
| 406 |
+
"""
|
| 407 |
+
Alias for get_current_user for backward compatibility.
|
| 408 |
+
|
| 409 |
+
Returns the user_info portion of the authentication data.
|
| 410 |
+
"""
|
| 411 |
+
auth_info = await get_current_user(authorization, redis_client)
|
| 412 |
+
return auth_info["user_info"]
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
async def get_authenticated_user_id(
|
| 416 |
+
authorization: Optional[str] = Header(None),
|
| 417 |
+
redis_client: Redis = Depends(get_redis)
|
| 418 |
+
) -> str:
|
| 419 |
+
"""
|
| 420 |
+
Get just the user ID from authentication.
|
| 421 |
+
|
| 422 |
+
Returns:
|
| 423 |
+
str: The authenticated user's ID
|
| 424 |
+
"""
|
| 425 |
+
auth_info = await get_current_user(authorization, redis_client)
|
| 426 |
+
return auth_info["user_info"]["id"]
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
async def get_verified_user(
|
| 430 |
+
authorization: Optional[str] = Header(None),
|
| 431 |
+
redis_client: Redis = Depends(get_redis)
|
| 432 |
+
) -> Dict[str, Any]:
|
| 433 |
+
"""
|
| 434 |
+
Get authenticated user that has verified email.
|
| 435 |
+
|
| 436 |
+
Returns:
|
| 437 |
+
Dict containing verified user information
|
| 438 |
+
|
| 439 |
+
Raises:
|
| 440 |
+
HTTPException: If user is not verified
|
| 441 |
+
"""
|
| 442 |
+
auth_info = await get_current_user(authorization, redis_client)
|
| 443 |
+
user_info = auth_info["user_info"]
|
| 444 |
+
|
| 445 |
+
# Check email verification status from email_addresses array
|
| 446 |
+
email_verified = user_info.get("email_verified", False)
|
| 447 |
+
|
| 448 |
+
# If not directly available, check in email_addresses array
|
| 449 |
+
if not email_verified and "email_addresses" in user_info:
|
| 450 |
+
email_addresses = user_info.get("email_addresses", [])
|
| 451 |
+
if email_addresses:
|
| 452 |
+
primary_email_id = user_info.get("primary_email_address_id")
|
| 453 |
+
if primary_email_id:
|
| 454 |
+
primary_email = next((e for e in email_addresses if e.get("id") == primary_email_id), None)
|
| 455 |
+
if primary_email:
|
| 456 |
+
verification = primary_email.get("verification", {})
|
| 457 |
+
email_verified = verification.get("status") == "verified"
|
| 458 |
+
|
| 459 |
+
# Fallback to first email verification
|
| 460 |
+
if not email_verified and email_addresses:
|
| 461 |
+
first_email = email_addresses[0]
|
| 462 |
+
verification = first_email.get("verification", {})
|
| 463 |
+
email_verified = verification.get("status") == "verified"
|
| 464 |
+
|
| 465 |
+
if not email_verified:
|
| 466 |
+
raise HTTPException(
|
| 467 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 468 |
+
detail="Email verification required"
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
return user_info
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
async def get_request_context(request: Request) -> Dict[str, Any]:
|
| 475 |
+
"""
|
| 476 |
+
Get request context information.
|
| 477 |
+
|
| 478 |
+
Args:
|
| 479 |
+
request: FastAPI request object
|
| 480 |
+
|
| 481 |
+
Returns:
|
| 482 |
+
Dict containing request context
|
| 483 |
+
"""
|
| 484 |
+
return {
|
| 485 |
+
"path": str(request.url.path),
|
| 486 |
+
"method": request.method,
|
| 487 |
+
"client_ip": request.client.host if request.client else "unknown",
|
| 488 |
+
"user_agent": request.headers.get("user-agent", "unknown"),
|
| 489 |
+
"timestamp": logger.info("Request context retrieved")
|
| 490 |
+
}
|
src/app/api/v1/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# API version 1
|
src/app/api/v1/auth.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication endpoints for testing Clerk integration.
|
| 3 |
+
|
| 4 |
+
This module provides endpoints for testing authentication functionality
|
| 5 |
+
and retrieving user information.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Dict, Any, Optional
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 12 |
+
from fastapi.responses import JSONResponse
|
| 13 |
+
|
| 14 |
+
from ...models.user import UserProfile, UserPermissions, UserRole, AuthResponse
|
| 15 |
+
from ...api.dependencies import (
|
| 16 |
+
get_authenticated_user,
|
| 17 |
+
get_authenticated_user_id,
|
| 18 |
+
get_verified_user,
|
| 19 |
+
get_optional_user,
|
| 20 |
+
get_request_context
|
| 21 |
+
)
|
| 22 |
+
from ...core.auth import clerk_manager
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@router.get("/me", response_model=UserProfile)
|
| 30 |
+
async def get_current_user_profile(
|
| 31 |
+
user_info: Dict[str, Any] = Depends(get_authenticated_user)
|
| 32 |
+
) -> UserProfile:
|
| 33 |
+
"""
|
| 34 |
+
Get current authenticated user profile.
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
UserProfile: Current user's profile information
|
| 38 |
+
"""
|
| 39 |
+
try:
|
| 40 |
+
# Handle potentially missing or invalid datetime fields from Clerk
|
| 41 |
+
def parse_datetime_field(value) -> Optional[datetime]:
|
| 42 |
+
"""Parse datetime field that might be None, timestamp (ms), or string."""
|
| 43 |
+
if value is None:
|
| 44 |
+
return None
|
| 45 |
+
if isinstance(value, datetime):
|
| 46 |
+
return value
|
| 47 |
+
if isinstance(value, (int, float)):
|
| 48 |
+
try:
|
| 49 |
+
# Convert milliseconds timestamp to datetime
|
| 50 |
+
return datetime.fromtimestamp(value / 1000)
|
| 51 |
+
except (ValueError, OSError):
|
| 52 |
+
return None
|
| 53 |
+
if isinstance(value, str):
|
| 54 |
+
try:
|
| 55 |
+
# Handle ISO format with Z suffix
|
| 56 |
+
return datetime.fromisoformat(value.replace('Z', '+00:00'))
|
| 57 |
+
except (ValueError, AttributeError):
|
| 58 |
+
return None
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
# Extract actual user data from nested structure
|
| 62 |
+
# Handle both nested ('user_info') and direct user data structures
|
| 63 |
+
if isinstance(user_info, dict) and 'user_info' in user_info:
|
| 64 |
+
# Nested structure from get_current_user
|
| 65 |
+
actual_user_info = user_info['user_info']
|
| 66 |
+
else:
|
| 67 |
+
# Direct user_info from get_verified_user
|
| 68 |
+
actual_user_info = user_info
|
| 69 |
+
|
| 70 |
+
# Debug logging to understand the Clerk API response structure
|
| 71 |
+
logger.debug(f"Processing user profile for user_id: {actual_user_info.get('id', 'unknown')}")
|
| 72 |
+
if "email_addresses" in actual_user_info:
|
| 73 |
+
logger.debug(f"Email addresses found: {len(actual_user_info['email_addresses'])} entries")
|
| 74 |
+
|
| 75 |
+
# Validate we have the required user ID
|
| 76 |
+
if not actual_user_info.get('id'):
|
| 77 |
+
logger.error(f"No user ID found in user_info: {user_info}")
|
| 78 |
+
raise HTTPException(
|
| 79 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 80 |
+
detail="Invalid user information"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Extract email from email_addresses array (Clerk standard structure)
|
| 84 |
+
email = actual_user_info.get("email")
|
| 85 |
+
email_verified = actual_user_info.get("email_verified", False)
|
| 86 |
+
|
| 87 |
+
# Look for email in email_addresses array if not found directly
|
| 88 |
+
email_addresses = actual_user_info.get("email_addresses", [])
|
| 89 |
+
if not email and email_addresses:
|
| 90 |
+
# Get primary email or first verified email
|
| 91 |
+
primary_email_id = actual_user_info.get("primary_email_address_id")
|
| 92 |
+
if primary_email_id:
|
| 93 |
+
primary_email = next((e for e in email_addresses if e.get("id") == primary_email_id), None)
|
| 94 |
+
if primary_email:
|
| 95 |
+
email = primary_email.get("email_address")
|
| 96 |
+
# Check verification status from the email object
|
| 97 |
+
verification = primary_email.get("verification", {})
|
| 98 |
+
email_verified = verification.get("status") == "verified"
|
| 99 |
+
|
| 100 |
+
# Fallback to first email if no primary found
|
| 101 |
+
if not email and email_addresses:
|
| 102 |
+
first_email = email_addresses[0]
|
| 103 |
+
email = first_email.get("email_address")
|
| 104 |
+
verification = first_email.get("verification", {})
|
| 105 |
+
email_verified = verification.get("status") == "verified"
|
| 106 |
+
|
| 107 |
+
# Build full name with proper fallbacks
|
| 108 |
+
first_name = actual_user_info.get('first_name') or ''
|
| 109 |
+
last_name = actual_user_info.get('last_name') or ''
|
| 110 |
+
full_name = f"{first_name} {last_name}".strip()
|
| 111 |
+
|
| 112 |
+
# Fallback chain for full_name
|
| 113 |
+
if not full_name:
|
| 114 |
+
full_name = actual_user_info.get("username") or email or "Unknown User"
|
| 115 |
+
|
| 116 |
+
# Convert Clerk user info to UserProfile
|
| 117 |
+
user_profile = UserProfile(
|
| 118 |
+
id=actual_user_info["id"],
|
| 119 |
+
username=actual_user_info.get("username"),
|
| 120 |
+
full_name=full_name,
|
| 121 |
+
email=email,
|
| 122 |
+
image_url=actual_user_info.get("image_url"),
|
| 123 |
+
email_verified=email_verified,
|
| 124 |
+
created_at=parse_datetime_field(actual_user_info.get("created_at")),
|
| 125 |
+
last_sign_in_at=parse_datetime_field(actual_user_info.get("last_sign_in_at"))
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
logger.info(f"User profile retrieved successfully for user {actual_user_info['id']}: {user_profile.dict()}")
|
| 129 |
+
return user_profile
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
logger.error(f"Failed to get user profile: {e}", exc_info=True)
|
| 133 |
+
raise HTTPException(
|
| 134 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 135 |
+
detail="Failed to retrieve user profile"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@router.get("/permissions")
|
| 140 |
+
async def get_user_permissions(
|
| 141 |
+
user_info: Dict[str, Any] = Depends(get_verified_user)
|
| 142 |
+
) -> UserPermissions:
|
| 143 |
+
"""
|
| 144 |
+
Get current user's permissions.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
UserPermissions: User's permissions and access levels
|
| 148 |
+
"""
|
| 149 |
+
try:
|
| 150 |
+
# Extract actual user data from nested structure
|
| 151 |
+
if isinstance(user_info, dict) and 'user_info' in user_info:
|
| 152 |
+
# Nested structure from get_current_user
|
| 153 |
+
actual_user_info = user_info['user_info']
|
| 154 |
+
else:
|
| 155 |
+
# Direct user_info from get_verified_user
|
| 156 |
+
actual_user_info = user_info
|
| 157 |
+
|
| 158 |
+
# Validate user_info structure
|
| 159 |
+
if not actual_user_info or not actual_user_info.get("id"):
|
| 160 |
+
logger.error(f"Invalid user_info structure: {user_info}")
|
| 161 |
+
raise HTTPException(
|
| 162 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 163 |
+
detail="Invalid user information"
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# For now, assign basic user role
|
| 167 |
+
# In a real implementation, you would check Clerk metadata or roles
|
| 168 |
+
user_role = UserRole.USER
|
| 169 |
+
|
| 170 |
+
# Check if user is admin (placeholder logic)
|
| 171 |
+
# This would be based on Clerk metadata or roles
|
| 172 |
+
|
| 173 |
+
# Extract email from email_addresses array (Clerk standard structure)
|
| 174 |
+
user_email = actual_user_info.get("email")
|
| 175 |
+
|
| 176 |
+
# Look for email in email_addresses array if not found directly
|
| 177 |
+
email_addresses = actual_user_info.get("email_addresses", [])
|
| 178 |
+
if not user_email and email_addresses:
|
| 179 |
+
# Get primary email or first email
|
| 180 |
+
primary_email_id = actual_user_info.get("primary_email_address_id")
|
| 181 |
+
if primary_email_id:
|
| 182 |
+
primary_email = next((e for e in email_addresses if e.get("id") == primary_email_id), None)
|
| 183 |
+
if primary_email:
|
| 184 |
+
user_email = primary_email.get("email_address")
|
| 185 |
+
|
| 186 |
+
# Fallback to first email if no primary found
|
| 187 |
+
if not user_email and email_addresses:
|
| 188 |
+
first_email = email_addresses[0]
|
| 189 |
+
user_email = first_email.get("email_address")
|
| 190 |
+
|
| 191 |
+
if user_email and user_email.endswith("@admin.com"):
|
| 192 |
+
user_role = UserRole.ADMIN
|
| 193 |
+
|
| 194 |
+
permissions = UserPermissions.from_user_role(
|
| 195 |
+
user_id=actual_user_info["id"],
|
| 196 |
+
role=user_role
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
logger.info(f"Permissions retrieved for user {actual_user_info['id']}: {user_role}")
|
| 200 |
+
return permissions
|
| 201 |
+
|
| 202 |
+
except Exception as e:
|
| 203 |
+
logger.error(f"Failed to get user permissions: {e}")
|
| 204 |
+
raise HTTPException(
|
| 205 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 206 |
+
detail="Failed to retrieve user permissions"
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
@router.get("/status")
|
| 211 |
+
async def get_auth_status(
|
| 212 |
+
user_info: Dict[str, Any] = Depends(get_optional_user),
|
| 213 |
+
request_context: Dict[str, Any] = Depends(get_request_context)
|
| 214 |
+
) -> Dict[str, Any]:
|
| 215 |
+
"""
|
| 216 |
+
Get authentication status for current request.
|
| 217 |
+
|
| 218 |
+
This endpoint works with or without authentication to show auth status.
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
Dict containing authentication status and user info if authenticated
|
| 222 |
+
"""
|
| 223 |
+
try:
|
| 224 |
+
# Debug logging to understand the structure
|
| 225 |
+
logger.info(f"Auth status check - user_info type: {type(user_info)}, content: {user_info}")
|
| 226 |
+
logger.info(f"Auth status check - request_context: {request_context}")
|
| 227 |
+
|
| 228 |
+
# Extract actual user data from nested structure
|
| 229 |
+
if isinstance(user_info, dict) and 'user_info' in user_info:
|
| 230 |
+
# Nested structure from get_current_user
|
| 231 |
+
actual_user_info = user_info['user_info']
|
| 232 |
+
elif user_info:
|
| 233 |
+
# Direct user_info from get_verified_user
|
| 234 |
+
actual_user_info = user_info
|
| 235 |
+
else:
|
| 236 |
+
actual_user_info = {}
|
| 237 |
+
|
| 238 |
+
if user_info and actual_user_info.get("id"):
|
| 239 |
+
# Extract email from email_addresses array (Clerk standard structure)
|
| 240 |
+
email = actual_user_info.get("email")
|
| 241 |
+
email_verified = actual_user_info.get("email_verified", False)
|
| 242 |
+
|
| 243 |
+
# Look for email in email_addresses array if not found directly
|
| 244 |
+
email_addresses = actual_user_info.get("email_addresses", [])
|
| 245 |
+
if not email and email_addresses:
|
| 246 |
+
# Get primary email or first email
|
| 247 |
+
primary_email_id = actual_user_info.get("primary_email_address_id")
|
| 248 |
+
if primary_email_id:
|
| 249 |
+
primary_email = next((e for e in email_addresses if e.get("id") == primary_email_id), None)
|
| 250 |
+
if primary_email:
|
| 251 |
+
email = primary_email.get("email_address")
|
| 252 |
+
# Check verification status from the email object
|
| 253 |
+
verification = primary_email.get("verification", {})
|
| 254 |
+
email_verified = verification.get("status") == "verified"
|
| 255 |
+
|
| 256 |
+
# Fallback to first email if no primary found
|
| 257 |
+
if not email and email_addresses:
|
| 258 |
+
first_email = email_addresses[0]
|
| 259 |
+
email = first_email.get("email_address")
|
| 260 |
+
verification = first_email.get("verification", {})
|
| 261 |
+
email_verified = verification.get("status") == "verified"
|
| 262 |
+
|
| 263 |
+
return {
|
| 264 |
+
"authenticated": True,
|
| 265 |
+
"user_id": actual_user_info.get("id"),
|
| 266 |
+
"email": email,
|
| 267 |
+
"email_verified": email_verified,
|
| 268 |
+
"username": actual_user_info.get("username"),
|
| 269 |
+
"request_context": {
|
| 270 |
+
"path": request_context.get("path", "unknown"),
|
| 271 |
+
"method": request_context.get("method", "unknown"),
|
| 272 |
+
"client_ip": request_context.get("client_ip", "unknown")
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
else:
|
| 276 |
+
return {
|
| 277 |
+
"authenticated": False,
|
| 278 |
+
"message": "No authentication provided",
|
| 279 |
+
"request_context": {
|
| 280 |
+
"path": request_context.get("path", "unknown"),
|
| 281 |
+
"method": request_context.get("method", "unknown"),
|
| 282 |
+
"client_ip": request_context.get("client_ip", "unknown")
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
logger.error(f"Failed to get auth status: {e}")
|
| 288 |
+
return {
|
| 289 |
+
"authenticated": False,
|
| 290 |
+
"error": f"Failed to determine authentication status: {str(e)}",
|
| 291 |
+
"request_context": {
|
| 292 |
+
"path": request_context.get("path", "unknown") if request_context else "unknown",
|
| 293 |
+
"method": request_context.get("method", "unknown") if request_context else "unknown",
|
| 294 |
+
"client_ip": request_context.get("client_ip", "unknown") if request_context else "unknown"
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
@router.post("/verify")
|
| 300 |
+
async def verify_token(
|
| 301 |
+
user_info: Dict[str, Any] = Depends(get_verified_user)
|
| 302 |
+
) -> Dict[str, Any]:
|
| 303 |
+
"""
|
| 304 |
+
Verify authentication token and return user information.
|
| 305 |
+
|
| 306 |
+
This endpoint requires a verified user (email verified).
|
| 307 |
+
|
| 308 |
+
Returns:
|
| 309 |
+
Dict containing verification status and user information
|
| 310 |
+
"""
|
| 311 |
+
try:
|
| 312 |
+
return {
|
| 313 |
+
"verified": True,
|
| 314 |
+
"user_id": user_info["id"],
|
| 315 |
+
"email": user_info.get("email"),
|
| 316 |
+
"email_verified": user_info.get("email_verified", False),
|
| 317 |
+
"message": "Token verified successfully"
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
except Exception as e:
|
| 321 |
+
logger.error(f"Token verification failed: {e}")
|
| 322 |
+
raise HTTPException(
|
| 323 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 324 |
+
detail="Token verification failed"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
@router.get("/health")
|
| 329 |
+
async def auth_health_check() -> Dict[str, Any]:
|
| 330 |
+
"""
|
| 331 |
+
Check authentication service health.
|
| 332 |
+
|
| 333 |
+
Returns:
|
| 334 |
+
Dict containing authentication service health status
|
| 335 |
+
"""
|
| 336 |
+
try:
|
| 337 |
+
# Check Clerk manager health
|
| 338 |
+
clerk_health = clerk_manager.health_check()
|
| 339 |
+
|
| 340 |
+
return {
|
| 341 |
+
"status": "healthy" if clerk_health["status"] == "healthy" else "unhealthy",
|
| 342 |
+
"clerk": clerk_health,
|
| 343 |
+
"message": "Authentication service health check completed"
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
except Exception as e:
|
| 347 |
+
logger.error(f"Auth health check failed: {e}")
|
| 348 |
+
return {
|
| 349 |
+
"status": "unhealthy",
|
| 350 |
+
"error": str(e),
|
| 351 |
+
"message": "Authentication service health check failed"
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@router.get("/test-protected")
|
| 356 |
+
async def test_protected_endpoint(
|
| 357 |
+
user_id: str = Depends(get_authenticated_user_id)
|
| 358 |
+
) -> Dict[str, Any]:
|
| 359 |
+
"""
|
| 360 |
+
Test endpoint that requires authentication.
|
| 361 |
+
|
| 362 |
+
Returns:
|
| 363 |
+
Dict containing success message and user ID
|
| 364 |
+
"""
|
| 365 |
+
logger.info(f"Protected endpoint accessed by user {user_id}")
|
| 366 |
+
|
| 367 |
+
return {
|
| 368 |
+
"message": "Successfully accessed protected endpoint",
|
| 369 |
+
"user_id": user_id,
|
| 370 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
@router.get("/test-verified")
|
| 375 |
+
async def test_verified_endpoint(
|
| 376 |
+
user_info: Dict[str, Any] = Depends(get_verified_user)
|
| 377 |
+
) -> Dict[str, Any]:
|
| 378 |
+
"""
|
| 379 |
+
Test endpoint that requires verified user.
|
| 380 |
+
|
| 381 |
+
Returns:
|
| 382 |
+
Dict containing success message and user information
|
| 383 |
+
"""
|
| 384 |
+
logger.info(f"Verified endpoint accessed by user {user_info['id']}")
|
| 385 |
+
|
| 386 |
+
return {
|
| 387 |
+
"message": "Successfully accessed verified user endpoint",
|
| 388 |
+
"user_id": user_info["id"],
|
| 389 |
+
"email": user_info.get("email"),
|
| 390 |
+
"email_verified": user_info.get("email_verified", False),
|
| 391 |
+
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 392 |
+
}
|
src/app/api/v1/files.py
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
File management API endpoints.
|
| 3 |
+
|
| 4 |
+
This module implements REST API endpoints for file operations including
|
| 5 |
+
upload, download, metadata retrieval, and file management.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Optional, List, Dict, Any
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from fastapi import (
|
| 13 |
+
APIRouter, Depends, HTTPException, status, UploadFile, File, Form,
|
| 14 |
+
Request, Response, Query
|
| 15 |
+
)
|
| 16 |
+
from fastapi.responses import StreamingResponse, FileResponse
|
| 17 |
+
from redis.asyncio import Redis
|
| 18 |
+
import aiofiles
|
| 19 |
+
|
| 20 |
+
from ...core.redis import get_redis
|
| 21 |
+
from ...core.auth import verify_clerk_token
|
| 22 |
+
from ...models.file import (
|
| 23 |
+
FileUploadRequest, FileUploadResponse, FileMetadataResponse,
|
| 24 |
+
FileListResponse, FileStatsResponse, FileCleanupRequest,
|
| 25 |
+
FileCleanupResponse, FileType, FileBatchUploadRequest,
|
| 26 |
+
FileBatchUploadResponse, FileSearchRequest
|
| 27 |
+
)
|
| 28 |
+
from ...models.common import PaginatedResponse
|
| 29 |
+
from ...services.file_service import FileService
|
| 30 |
+
from ...api.dependencies import get_current_user, get_file_service
|
| 31 |
+
from ...utils.file_utils import FileMetadata
|
| 32 |
+
|
| 33 |
+
logger = logging.getLogger(__name__)
|
| 34 |
+
router = APIRouter(prefix="/files", tags=["files"])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.post("/upload", response_model=FileUploadResponse, status_code=status.HTTP_201_CREATED)
|
| 38 |
+
async def upload_file(
|
| 39 |
+
file: UploadFile = File(..., description="File to upload"),
|
| 40 |
+
file_type: Optional[FileType] = Form(None, description="File type category"),
|
| 41 |
+
subdirectory: Optional[str] = Form(None, description="Optional subdirectory"),
|
| 42 |
+
description: Optional[str] = Form(None, description="File description"),
|
| 43 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 44 |
+
file_service: FileService = Depends(get_file_service)
|
| 45 |
+
) -> FileUploadResponse:
|
| 46 |
+
"""
|
| 47 |
+
Upload a single file.
|
| 48 |
+
|
| 49 |
+
This endpoint accepts a file upload with optional metadata and stores it
|
| 50 |
+
securely with proper validation and access controls.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
file: File to upload
|
| 54 |
+
file_type: Optional file type category
|
| 55 |
+
subdirectory: Optional subdirectory for organization
|
| 56 |
+
description: Optional file description
|
| 57 |
+
current_user: Authenticated user information
|
| 58 |
+
file_service: File service dependency
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
FileUploadResponse with upload details
|
| 62 |
+
|
| 63 |
+
Raises:
|
| 64 |
+
HTTPException: If upload fails or validation errors occur
|
| 65 |
+
"""
|
| 66 |
+
try:
|
| 67 |
+
logger.info(
|
| 68 |
+
"Starting file upload",
|
| 69 |
+
filename=file.filename,
|
| 70 |
+
content_type=file.content_type,
|
| 71 |
+
user_id=current_user["user_info"]["id"],
|
| 72 |
+
file_type=file_type
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Upload file
|
| 76 |
+
file_metadata = await file_service.upload_file(
|
| 77 |
+
file=file,
|
| 78 |
+
user_id=current_user["user_info"]["id"],
|
| 79 |
+
file_type=file_type.value if file_type else None,
|
| 80 |
+
subdirectory=subdirectory
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Generate download URL
|
| 84 |
+
download_url = await file_service.generate_download_url(
|
| 85 |
+
file_id=Path(file_metadata.filename).stem,
|
| 86 |
+
user_id=current_user["user_info"]["id"]
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
logger.info(
|
| 90 |
+
"File uploaded successfully",
|
| 91 |
+
file_id=Path(file_metadata.filename).stem,
|
| 92 |
+
filename=file_metadata.filename,
|
| 93 |
+
file_size=file_metadata.file_size,
|
| 94 |
+
user_id=current_user["user_info"]["id"]
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
return FileUploadResponse(
|
| 98 |
+
file_id=Path(file_metadata.filename).stem,
|
| 99 |
+
filename=file_metadata.filename,
|
| 100 |
+
file_type=FileType(file_metadata.file_type),
|
| 101 |
+
file_size=file_metadata.file_size,
|
| 102 |
+
download_url=download_url or f"/api/v1/files/{Path(file_metadata.filename).stem}/download",
|
| 103 |
+
created_at=file_metadata.created_at
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
except HTTPException:
|
| 107 |
+
raise
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.error(
|
| 110 |
+
"Failed to upload file",
|
| 111 |
+
filename=file.filename,
|
| 112 |
+
user_id=current_user["user_info"]["id"],
|
| 113 |
+
error=str(e),
|
| 114 |
+
exc_info=True
|
| 115 |
+
)
|
| 116 |
+
raise HTTPException(
|
| 117 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 118 |
+
detail=f"Failed to upload file: {str(e)}"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@router.post("/batch-upload", response_model=FileBatchUploadResponse)
|
| 123 |
+
async def batch_upload_files(
|
| 124 |
+
files: List[UploadFile] = File(..., description="Files to upload"),
|
| 125 |
+
file_type: Optional[FileType] = Form(None, description="File type for all files"),
|
| 126 |
+
subdirectory: Optional[str] = Form(None, description="Subdirectory for all files"),
|
| 127 |
+
description: Optional[str] = Form(None, description="Description for all files"),
|
| 128 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 129 |
+
file_service: FileService = Depends(get_file_service)
|
| 130 |
+
) -> FileBatchUploadResponse:
|
| 131 |
+
"""
|
| 132 |
+
Upload multiple files in batch.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
files: List of files to upload
|
| 136 |
+
file_type: Optional file type for all files
|
| 137 |
+
subdirectory: Optional subdirectory for all files
|
| 138 |
+
description: Optional description for all files
|
| 139 |
+
current_user: Authenticated user information
|
| 140 |
+
file_service: File service dependency
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
FileBatchUploadResponse with batch upload results
|
| 144 |
+
"""
|
| 145 |
+
try:
|
| 146 |
+
if len(files) > 50:
|
| 147 |
+
raise HTTPException(
|
| 148 |
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 149 |
+
detail="Maximum 50 files allowed per batch"
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
successful_uploads = []
|
| 153 |
+
failed_uploads = []
|
| 154 |
+
|
| 155 |
+
logger.info(
|
| 156 |
+
"Starting batch file upload",
|
| 157 |
+
file_count=len(files),
|
| 158 |
+
user_id=current_user["user_info"]["id"]
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
for file in files:
|
| 162 |
+
try:
|
| 163 |
+
# Upload individual file
|
| 164 |
+
file_metadata = await file_service.upload_file(
|
| 165 |
+
file=file,
|
| 166 |
+
user_id=current_user["user_info"]["id"],
|
| 167 |
+
file_type=file_type.value if file_type else None,
|
| 168 |
+
subdirectory=subdirectory
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# Generate download URL
|
| 172 |
+
download_url = await file_service.generate_download_url(
|
| 173 |
+
file_id=Path(file_metadata.filename).stem,
|
| 174 |
+
user_id=current_user["user_info"]["id"]
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
successful_uploads.append(FileUploadResponse(
|
| 178 |
+
file_id=Path(file_metadata.filename).stem,
|
| 179 |
+
filename=file_metadata.filename,
|
| 180 |
+
file_type=FileType(file_metadata.file_type),
|
| 181 |
+
file_size=file_metadata.file_size,
|
| 182 |
+
download_url=download_url or f"/api/v1/files/{Path(file_metadata.filename).stem}/download",
|
| 183 |
+
created_at=file_metadata.created_at
|
| 184 |
+
))
|
| 185 |
+
|
| 186 |
+
except Exception as e:
|
| 187 |
+
failed_uploads.append({
|
| 188 |
+
"filename": file.filename,
|
| 189 |
+
"error": str(e),
|
| 190 |
+
"error_type": type(e).__name__
|
| 191 |
+
})
|
| 192 |
+
|
| 193 |
+
logger.info(
|
| 194 |
+
"Batch upload completed",
|
| 195 |
+
total_files=len(files),
|
| 196 |
+
successful=len(successful_uploads),
|
| 197 |
+
failed=len(failed_uploads),
|
| 198 |
+
user_id=current_user["user_info"]["id"]
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
return FileBatchUploadResponse(
|
| 202 |
+
successful_uploads=successful_uploads,
|
| 203 |
+
failed_uploads=failed_uploads,
|
| 204 |
+
total_files=len(files),
|
| 205 |
+
success_count=len(successful_uploads),
|
| 206 |
+
failure_count=len(failed_uploads)
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
except HTTPException:
|
| 210 |
+
raise
|
| 211 |
+
except Exception as e:
|
| 212 |
+
logger.error(f"Batch upload failed: {e}", exc_info=True)
|
| 213 |
+
raise HTTPException(
|
| 214 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 215 |
+
detail=f"Batch upload failed: {str(e)}"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
@router.get("/{file_id}/download")
|
| 220 |
+
async def download_file(
|
| 221 |
+
file_id: str,
|
| 222 |
+
request: Request,
|
| 223 |
+
inline: bool = Query(False, description="Serve file inline instead of attachment"),
|
| 224 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 225 |
+
file_service: FileService = Depends(get_file_service)
|
| 226 |
+
) -> StreamingResponse:
|
| 227 |
+
"""
|
| 228 |
+
Download a file by ID with range request support and caching.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
file_id: File identifier
|
| 232 |
+
request: FastAPI request object
|
| 233 |
+
inline: Whether to serve inline or as attachment
|
| 234 |
+
current_user: Authenticated user information
|
| 235 |
+
file_service: File service dependency
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
StreamingResponse with file content
|
| 239 |
+
|
| 240 |
+
Raises:
|
| 241 |
+
HTTPException: If file not found or access denied
|
| 242 |
+
"""
|
| 243 |
+
try:
|
| 244 |
+
# Get file metadata
|
| 245 |
+
file_metadata = await file_service.get_file_metadata(
|
| 246 |
+
file_id=file_id,
|
| 247 |
+
user_id=current_user["user_info"]["id"]
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
if not file_metadata:
|
| 251 |
+
raise HTTPException(
|
| 252 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 253 |
+
detail="File not found"
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
logger.info(
|
| 257 |
+
"Serving file download",
|
| 258 |
+
file_id=file_id,
|
| 259 |
+
filename=file_metadata.filename,
|
| 260 |
+
user_id=current_user["user_info"]["id"],
|
| 261 |
+
inline=inline
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Track file access
|
| 265 |
+
from ...utils.file_cache import track_file_access
|
| 266 |
+
await track_file_access(
|
| 267 |
+
file_id=file_id,
|
| 268 |
+
user_id=current_user["user_info"]["id"],
|
| 269 |
+
access_type="download"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Use enhanced file serving with user context
|
| 273 |
+
from ...utils.file_serving import serve_file_secure
|
| 274 |
+
|
| 275 |
+
return await serve_file_secure(
|
| 276 |
+
file_path=file_metadata.file_path,
|
| 277 |
+
file_type=file_metadata.file_type,
|
| 278 |
+
original_filename=file_metadata.original_filename,
|
| 279 |
+
request=request,
|
| 280 |
+
inline=inline,
|
| 281 |
+
enable_caching=True,
|
| 282 |
+
user_id=current_user["user_info"]["id"]
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
except HTTPException:
|
| 286 |
+
raise
|
| 287 |
+
except Exception as e:
|
| 288 |
+
logger.error(f"Failed to download file: {e}", exc_info=True)
|
| 289 |
+
raise HTTPException(
|
| 290 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 291 |
+
detail=f"Failed to download file: {str(e)}"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
@router.get("/{file_id}/metadata", response_model=FileMetadataResponse)
|
| 296 |
+
async def get_file_metadata(
|
| 297 |
+
file_id: str,
|
| 298 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 299 |
+
file_service: FileService = Depends(get_file_service)
|
| 300 |
+
) -> FileMetadataResponse:
|
| 301 |
+
"""
|
| 302 |
+
Get file metadata by ID.
|
| 303 |
+
|
| 304 |
+
Args:
|
| 305 |
+
file_id: File identifier
|
| 306 |
+
current_user: Authenticated user information
|
| 307 |
+
file_service: File service dependency
|
| 308 |
+
|
| 309 |
+
Returns:
|
| 310 |
+
FileMetadataResponse with file metadata
|
| 311 |
+
|
| 312 |
+
Raises:
|
| 313 |
+
HTTPException: If file not found or access denied
|
| 314 |
+
"""
|
| 315 |
+
try:
|
| 316 |
+
file_metadata = await file_service.get_file_metadata(
|
| 317 |
+
file_id=file_id,
|
| 318 |
+
user_id=current_user["user_info"]["id"]
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
if not file_metadata:
|
| 322 |
+
raise HTTPException(
|
| 323 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 324 |
+
detail="File not found"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# Generate download URL
|
| 328 |
+
download_url = await file_service.generate_download_url(
|
| 329 |
+
file_id=file_id,
|
| 330 |
+
user_id=current_user["user_info"]["id"]
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
return FileMetadataResponse(
|
| 334 |
+
id=file_id,
|
| 335 |
+
filename=file_metadata.filename,
|
| 336 |
+
original_filename=file_metadata.original_filename,
|
| 337 |
+
file_type=FileType(file_metadata.file_type),
|
| 338 |
+
mime_type=file_metadata.mime_type,
|
| 339 |
+
file_size=file_metadata.file_size,
|
| 340 |
+
checksum=file_metadata.checksum,
|
| 341 |
+
created_at=file_metadata.created_at,
|
| 342 |
+
download_url=download_url,
|
| 343 |
+
metadata=file_metadata.metadata
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
except HTTPException:
|
| 347 |
+
raise
|
| 348 |
+
except Exception as e:
|
| 349 |
+
logger.error(f"Failed to get file metadata: {e}", exc_info=True)
|
| 350 |
+
raise HTTPException(
|
| 351 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 352 |
+
detail=f"Failed to get file metadata: {str(e)}"
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
@router.get("", response_model=FileListResponse)
|
| 357 |
+
async def list_files(
|
| 358 |
+
file_type: Optional[FileType] = Query(None, description="Filter by file type"),
|
| 359 |
+
page: int = Query(1, ge=1, description="Page number"),
|
| 360 |
+
items_per_page: int = Query(20, ge=1, le=100, description="Items per page"),
|
| 361 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 362 |
+
file_service: FileService = Depends(get_file_service)
|
| 363 |
+
) -> FileListResponse:
|
| 364 |
+
"""
|
| 365 |
+
List user's files with pagination and filtering.
|
| 366 |
+
|
| 367 |
+
Args:
|
| 368 |
+
file_type: Optional file type filter
|
| 369 |
+
page: Page number
|
| 370 |
+
items_per_page: Items per page
|
| 371 |
+
current_user: Authenticated user information
|
| 372 |
+
file_service: File service dependency
|
| 373 |
+
|
| 374 |
+
Returns:
|
| 375 |
+
FileListResponse with paginated file list
|
| 376 |
+
"""
|
| 377 |
+
try:
|
| 378 |
+
# Get paginated files
|
| 379 |
+
paginated_response = await file_service.get_user_files(
|
| 380 |
+
user_id=current_user["user_info"]["id"],
|
| 381 |
+
file_type=file_type.value if file_type else None,
|
| 382 |
+
page=page,
|
| 383 |
+
items_per_page=items_per_page
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
# Convert to response format
|
| 387 |
+
files = []
|
| 388 |
+
for file_metadata in paginated_response.data:
|
| 389 |
+
download_url = await file_service.generate_download_url(
|
| 390 |
+
file_id=Path(file_metadata.filename).stem,
|
| 391 |
+
user_id=current_user["user_info"]["id"]
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
files.append(FileMetadataResponse(
|
| 395 |
+
id=Path(file_metadata.filename).stem,
|
| 396 |
+
filename=file_metadata.filename,
|
| 397 |
+
original_filename=file_metadata.original_filename,
|
| 398 |
+
file_type=FileType(file_metadata.file_type),
|
| 399 |
+
mime_type=file_metadata.mime_type,
|
| 400 |
+
file_size=file_metadata.file_size,
|
| 401 |
+
checksum=file_metadata.checksum,
|
| 402 |
+
created_at=file_metadata.created_at,
|
| 403 |
+
download_url=download_url,
|
| 404 |
+
metadata=file_metadata.metadata
|
| 405 |
+
))
|
| 406 |
+
|
| 407 |
+
return FileListResponse(
|
| 408 |
+
files=files,
|
| 409 |
+
total_count=paginated_response.total_count,
|
| 410 |
+
page=page,
|
| 411 |
+
items_per_page=items_per_page,
|
| 412 |
+
has_next=page * items_per_page < paginated_response.total_count,
|
| 413 |
+
has_previous=page > 1
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
except Exception as e:
|
| 417 |
+
logger.error(f"Failed to list files: {e}", exc_info=True)
|
| 418 |
+
raise HTTPException(
|
| 419 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 420 |
+
detail=f"Failed to list files: {str(e)}"
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
@router.delete("/{file_id}")
|
| 425 |
+
async def delete_file(
|
| 426 |
+
file_id: str,
|
| 427 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 428 |
+
file_service: FileService = Depends(get_file_service)
|
| 429 |
+
) -> Dict[str, Any]:
|
| 430 |
+
"""
|
| 431 |
+
Delete a file by ID.
|
| 432 |
+
|
| 433 |
+
Args:
|
| 434 |
+
file_id: File identifier
|
| 435 |
+
current_user: Authenticated user information
|
| 436 |
+
file_service: File service dependency
|
| 437 |
+
|
| 438 |
+
Returns:
|
| 439 |
+
Success message
|
| 440 |
+
|
| 441 |
+
Raises:
|
| 442 |
+
HTTPException: If file not found or deletion fails
|
| 443 |
+
"""
|
| 444 |
+
try:
|
| 445 |
+
success = await file_service.delete_file(
|
| 446 |
+
file_id=file_id,
|
| 447 |
+
user_id=current_user["user_info"]["id"]
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
if not success:
|
| 451 |
+
raise HTTPException(
|
| 452 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 453 |
+
detail="File not found or already deleted"
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
logger.info(
|
| 457 |
+
"File deleted successfully",
|
| 458 |
+
file_id=file_id,
|
| 459 |
+
user_id=current_user["user_info"]["id"]
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
return {"message": "File deleted successfully", "file_id": file_id}
|
| 463 |
+
|
| 464 |
+
except HTTPException:
|
| 465 |
+
raise
|
| 466 |
+
except Exception as e:
|
| 467 |
+
logger.error(f"Failed to delete file: {e}", exc_info=True)
|
| 468 |
+
raise HTTPException(
|
| 469 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 470 |
+
detail=f"Failed to delete file: {str(e)}"
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
@router.get("/stats", response_model=FileStatsResponse)
|
| 475 |
+
async def get_file_stats(
|
| 476 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 477 |
+
file_service: FileService = Depends(get_file_service)
|
| 478 |
+
) -> FileStatsResponse:
|
| 479 |
+
"""
|
| 480 |
+
Get file statistics for the current user.
|
| 481 |
+
|
| 482 |
+
Args:
|
| 483 |
+
current_user: Authenticated user information
|
| 484 |
+
file_service: File service dependency
|
| 485 |
+
|
| 486 |
+
Returns:
|
| 487 |
+
FileStatsResponse with user's file statistics
|
| 488 |
+
"""
|
| 489 |
+
try:
|
| 490 |
+
stats = await file_service.get_file_stats(
|
| 491 |
+
user_id=current_user["user_info"]["id"]
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
return FileStatsResponse(
|
| 495 |
+
total_files=stats.get("total_files", 0),
|
| 496 |
+
total_size=stats.get("total_size", 0),
|
| 497 |
+
by_type=stats.get("by_type", {}),
|
| 498 |
+
recent_uploads=stats.get("recent_uploads", 0)
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
except Exception as e:
|
| 502 |
+
logger.error(f"Failed to get file stats: {e}", exc_info=True)
|
| 503 |
+
raise HTTPException(
|
| 504 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 505 |
+
detail=f"Failed to get file stats: {str(e)}"
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
@router.post("/cleanup", response_model=FileCleanupResponse)
|
| 510 |
+
async def cleanup_files(
|
| 511 |
+
request: FileCleanupRequest,
|
| 512 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 513 |
+
file_service: FileService = Depends(get_file_service)
|
| 514 |
+
) -> FileCleanupResponse:
|
| 515 |
+
"""
|
| 516 |
+
Clean up old files (admin only).
|
| 517 |
+
|
| 518 |
+
Args:
|
| 519 |
+
request: Cleanup request parameters
|
| 520 |
+
current_user: Authenticated user information
|
| 521 |
+
file_service: File service dependency
|
| 522 |
+
|
| 523 |
+
Returns:
|
| 524 |
+
FileCleanupResponse with cleanup results
|
| 525 |
+
|
| 526 |
+
Raises:
|
| 527 |
+
HTTPException: If user is not admin or cleanup fails
|
| 528 |
+
"""
|
| 529 |
+
try:
|
| 530 |
+
# Check if user is admin (you may need to implement admin role checking)
|
| 531 |
+
user_roles = current_user.get("user_info", {}).get("roles", [])
|
| 532 |
+
if "admin" not in user_roles:
|
| 533 |
+
raise HTTPException(
|
| 534 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 535 |
+
detail="Admin access required"
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
if request.dry_run:
|
| 539 |
+
# For dry run, just return what would be cleaned
|
| 540 |
+
return FileCleanupResponse(
|
| 541 |
+
files_cleaned=0,
|
| 542 |
+
metadata_cleaned=0,
|
| 543 |
+
retention_days=request.retention_days,
|
| 544 |
+
dry_run=True
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
# Perform cleanup
|
| 548 |
+
cleanup_stats = await file_service.cleanup_expired_files(
|
| 549 |
+
retention_days=request.retention_days
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
logger.info(
|
| 553 |
+
"File cleanup completed",
|
| 554 |
+
**cleanup_stats,
|
| 555 |
+
user_id=current_user["user_info"]["id"]
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
return FileCleanupResponse(
|
| 559 |
+
files_cleaned=cleanup_stats.get("files_cleaned", 0),
|
| 560 |
+
metadata_cleaned=cleanup_stats.get("metadata_cleaned", 0),
|
| 561 |
+
retention_days=request.retention_days,
|
| 562 |
+
dry_run=False
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
except HTTPException:
|
| 566 |
+
raise
|
| 567 |
+
except Exception as e:
|
| 568 |
+
logger.error(f"Failed to cleanup files: {e}", exc_info=True)
|
| 569 |
+
raise HTTPException(
|
| 570 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 571 |
+
detail=f"Failed to cleanup files: {str(e)}"
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
@router.get("/{file_id}/thumbnail")
|
| 576 |
+
async def get_file_thumbnail(
|
| 577 |
+
file_id: str,
|
| 578 |
+
size: str = Query("medium", pattern="^(small|medium|large)$", description="Thumbnail size"),
|
| 579 |
+
request: Request = None,
|
| 580 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 581 |
+
file_service: FileService = Depends(get_file_service)
|
| 582 |
+
) -> StreamingResponse:
|
| 583 |
+
"""
|
| 584 |
+
Get thumbnail for image or video file.
|
| 585 |
+
|
| 586 |
+
Args:
|
| 587 |
+
file_id: File identifier
|
| 588 |
+
size: Thumbnail size (small, medium, large)
|
| 589 |
+
request: FastAPI request object
|
| 590 |
+
current_user: Authenticated user information
|
| 591 |
+
file_service: File service dependency
|
| 592 |
+
|
| 593 |
+
Returns:
|
| 594 |
+
StreamingResponse with thumbnail image
|
| 595 |
+
|
| 596 |
+
Raises:
|
| 597 |
+
HTTPException: If file not found or thumbnail not available
|
| 598 |
+
"""
|
| 599 |
+
try:
|
| 600 |
+
# Get file metadata
|
| 601 |
+
file_metadata = await file_service.get_file_metadata(
|
| 602 |
+
file_id=file_id,
|
| 603 |
+
user_id=current_user["user_info"]["id"]
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
if not file_metadata:
|
| 607 |
+
raise HTTPException(
|
| 608 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 609 |
+
detail="File not found"
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
# Check if file type supports thumbnails
|
| 613 |
+
if file_metadata.file_type not in ['image', 'video']:
|
| 614 |
+
raise HTTPException(
|
| 615 |
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 616 |
+
detail="Thumbnails not available for this file type"
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
# For now, return a placeholder response
|
| 620 |
+
# In a real implementation, you would generate/serve actual thumbnails
|
| 621 |
+
raise HTTPException(
|
| 622 |
+
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
| 623 |
+
detail="Thumbnail generation not yet implemented"
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
except HTTPException:
|
| 627 |
+
raise
|
| 628 |
+
except Exception as e:
|
| 629 |
+
logger.error(f"Failed to get thumbnail: {e}", exc_info=True)
|
| 630 |
+
raise HTTPException(
|
| 631 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 632 |
+
detail=f"Failed to get thumbnail: {str(e)}"
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
@router.get("/{file_id}/stream")
|
| 637 |
+
async def stream_file(
|
| 638 |
+
file_id: str,
|
| 639 |
+
quality: str = Query("auto", description="Stream quality"),
|
| 640 |
+
request: Request = None,
|
| 641 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 642 |
+
file_service: FileService = Depends(get_file_service)
|
| 643 |
+
) -> StreamingResponse:
|
| 644 |
+
"""
|
| 645 |
+
Stream video or audio file with adaptive quality.
|
| 646 |
+
|
| 647 |
+
Args:
|
| 648 |
+
file_id: File identifier
|
| 649 |
+
quality: Stream quality setting
|
| 650 |
+
request: FastAPI request object
|
| 651 |
+
current_user: Authenticated user information
|
| 652 |
+
file_service: File service dependency
|
| 653 |
+
|
| 654 |
+
Returns:
|
| 655 |
+
StreamingResponse with media stream
|
| 656 |
+
|
| 657 |
+
Raises:
|
| 658 |
+
HTTPException: If file not found or streaming not supported
|
| 659 |
+
"""
|
| 660 |
+
try:
|
| 661 |
+
# Get file metadata
|
| 662 |
+
file_metadata = await file_service.get_file_metadata(
|
| 663 |
+
file_id=file_id,
|
| 664 |
+
user_id=current_user["user_info"]["id"]
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
if not file_metadata:
|
| 668 |
+
raise HTTPException(
|
| 669 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 670 |
+
detail="File not found"
|
| 671 |
+
)
|
| 672 |
+
|
| 673 |
+
# Check if file type supports streaming
|
| 674 |
+
if file_metadata.file_type not in ['video', 'audio']:
|
| 675 |
+
raise HTTPException(
|
| 676 |
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 677 |
+
detail="Streaming not available for this file type"
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
logger.info(
|
| 681 |
+
"Serving file stream",
|
| 682 |
+
file_id=file_id,
|
| 683 |
+
filename=file_metadata.filename,
|
| 684 |
+
quality=quality,
|
| 685 |
+
user_id=current_user["user_info"]["id"]
|
| 686 |
+
)
|
| 687 |
+
|
| 688 |
+
# Track file access
|
| 689 |
+
from ...utils.file_cache import track_file_access
|
| 690 |
+
await track_file_access(
|
| 691 |
+
file_id=file_id,
|
| 692 |
+
user_id=current_user["user_info"]["id"],
|
| 693 |
+
access_type="stream"
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
# Use enhanced file serving with inline=True for streaming
|
| 697 |
+
from ...utils.file_serving import serve_file_secure
|
| 698 |
+
|
| 699 |
+
return await serve_file_secure(
|
| 700 |
+
file_path=file_metadata.file_path,
|
| 701 |
+
file_type=file_metadata.file_type,
|
| 702 |
+
original_filename=file_metadata.original_filename,
|
| 703 |
+
request=request,
|
| 704 |
+
inline=True, # Serve inline for streaming
|
| 705 |
+
enable_caching=True
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
except HTTPException:
|
| 709 |
+
raise
|
| 710 |
+
except Exception as e:
|
| 711 |
+
logger.error(f"Failed to stream file: {e}", exc_info=True)
|
| 712 |
+
raise HTTPException(
|
| 713 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 714 |
+
detail=f"Failed to stream file: {str(e)}"
|
| 715 |
+
)
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
@router.get("/{file_id}/info")
|
| 719 |
+
async def get_file_info(
|
| 720 |
+
file_id: str,
|
| 721 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 722 |
+
file_service: FileService = Depends(get_file_service)
|
| 723 |
+
) -> Dict[str, Any]:
|
| 724 |
+
"""
|
| 725 |
+
Get comprehensive file information including URLs.
|
| 726 |
+
|
| 727 |
+
Args:
|
| 728 |
+
file_id: File identifier
|
| 729 |
+
current_user: Authenticated user information
|
| 730 |
+
file_service: File service dependency
|
| 731 |
+
|
| 732 |
+
Returns:
|
| 733 |
+
Dictionary with file information and access URLs
|
| 734 |
+
|
| 735 |
+
Raises:
|
| 736 |
+
HTTPException: If file not found or access denied
|
| 737 |
+
"""
|
| 738 |
+
try:
|
| 739 |
+
file_metadata = await file_service.get_file_metadata(
|
| 740 |
+
file_id=file_id,
|
| 741 |
+
user_id=current_user["user_info"]["id"]
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
if not file_metadata:
|
| 745 |
+
raise HTTPException(
|
| 746 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 747 |
+
detail="File not found"
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
# Generate various URLs
|
| 751 |
+
from ...utils.file_serving import (
|
| 752 |
+
generate_secure_file_url,
|
| 753 |
+
generate_thumbnail_url,
|
| 754 |
+
generate_streaming_url
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
urls = {
|
| 758 |
+
"download": generate_secure_file_url(file_id, file_metadata.file_type),
|
| 759 |
+
"download_inline": generate_secure_file_url(file_id, file_metadata.file_type, inline=True),
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
# Add type-specific URLs
|
| 763 |
+
if file_metadata.file_type in ['image', 'video']:
|
| 764 |
+
urls["thumbnail_small"] = generate_thumbnail_url(file_id, "small")
|
| 765 |
+
urls["thumbnail_medium"] = generate_thumbnail_url(file_id, "medium")
|
| 766 |
+
urls["thumbnail_large"] = generate_thumbnail_url(file_id, "large")
|
| 767 |
+
|
| 768 |
+
if file_metadata.file_type in ['video', 'audio']:
|
| 769 |
+
urls["stream"] = generate_streaming_url(file_id)
|
| 770 |
+
|
| 771 |
+
return {
|
| 772 |
+
"file_id": file_id,
|
| 773 |
+
"filename": file_metadata.filename,
|
| 774 |
+
"original_filename": file_metadata.original_filename,
|
| 775 |
+
"file_type": file_metadata.file_type,
|
| 776 |
+
"mime_type": file_metadata.mime_type,
|
| 777 |
+
"file_size": file_metadata.file_size,
|
| 778 |
+
"created_at": file_metadata.created_at.isoformat(),
|
| 779 |
+
"metadata": file_metadata.metadata,
|
| 780 |
+
"urls": urls
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
except HTTPException:
|
| 784 |
+
raise
|
| 785 |
+
except Exception as e:
|
| 786 |
+
logger.error(f"Failed to get file info: {e}", exc_info=True)
|
| 787 |
+
raise HTTPException(
|
| 788 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 789 |
+
detail=f"Failed to get file info: {str(e)}"
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
|
| 793 |
+
@router.get("/{file_id}/analytics")
|
| 794 |
+
async def get_file_analytics(
|
| 795 |
+
file_id: str,
|
| 796 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 797 |
+
file_service: FileService = Depends(get_file_service)
|
| 798 |
+
) -> Dict[str, Any]:
|
| 799 |
+
"""
|
| 800 |
+
Get file access analytics and statistics.
|
| 801 |
+
|
| 802 |
+
Args:
|
| 803 |
+
file_id: File identifier
|
| 804 |
+
current_user: Authenticated user information
|
| 805 |
+
file_service: File service dependency
|
| 806 |
+
|
| 807 |
+
Returns:
|
| 808 |
+
Dictionary with file analytics
|
| 809 |
+
|
| 810 |
+
Raises:
|
| 811 |
+
HTTPException: If file not found or access denied
|
| 812 |
+
"""
|
| 813 |
+
try:
|
| 814 |
+
# Verify file ownership
|
| 815 |
+
file_metadata = await file_service.get_file_metadata(
|
| 816 |
+
file_id=file_id,
|
| 817 |
+
user_id=current_user["user_info"]["id"]
|
| 818 |
+
)
|
| 819 |
+
|
| 820 |
+
if not file_metadata:
|
| 821 |
+
raise HTTPException(
|
| 822 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 823 |
+
detail="File not found"
|
| 824 |
+
)
|
| 825 |
+
|
| 826 |
+
# Get access statistics
|
| 827 |
+
from ...utils.file_cache import get_file_access_statistics
|
| 828 |
+
access_stats = await get_file_access_statistics(file_id)
|
| 829 |
+
|
| 830 |
+
return {
|
| 831 |
+
"file_id": file_id,
|
| 832 |
+
"filename": file_metadata.filename,
|
| 833 |
+
"access_statistics": access_stats,
|
| 834 |
+
"file_size": file_metadata.file_size,
|
| 835 |
+
"created_at": file_metadata.created_at.isoformat()
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
except HTTPException:
|
| 839 |
+
raise
|
| 840 |
+
except Exception as e:
|
| 841 |
+
logger.error(f"Failed to get file analytics: {e}", exc_info=True)
|
| 842 |
+
raise HTTPException(
|
| 843 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 844 |
+
detail=f"Failed to get file analytics: {str(e)}"
|
| 845 |
+
)
|
| 846 |
+
|
| 847 |
+
|
| 848 |
+
@router.get("/{file_id}/secure")
|
| 849 |
+
async def secure_file_access(
|
| 850 |
+
file_id: str,
|
| 851 |
+
request: Request,
|
| 852 |
+
user_id: str = Query(..., description="User ID from signed URL"),
|
| 853 |
+
expires: str = Query(..., description="Expiration timestamp"),
|
| 854 |
+
signature: str = Query(..., description="URL signature"),
|
| 855 |
+
file_type: Optional[str] = Query(None, description="File type"),
|
| 856 |
+
inline: Optional[str] = Query("false", description="Serve inline"),
|
| 857 |
+
size: Optional[str] = Query(None, description="Thumbnail size"),
|
| 858 |
+
quality: Optional[str] = Query(None, description="Stream quality"),
|
| 859 |
+
file_service: FileService = Depends(get_file_service)
|
| 860 |
+
) -> StreamingResponse:
|
| 861 |
+
"""
|
| 862 |
+
Secure file access endpoint using signed URLs.
|
| 863 |
+
|
| 864 |
+
This endpoint provides secure file access without requiring authentication
|
| 865 |
+
headers, using signed URLs with expiration and integrity verification.
|
| 866 |
+
|
| 867 |
+
Args:
|
| 868 |
+
file_id: File identifier
|
| 869 |
+
request: FastAPI request object
|
| 870 |
+
user_id: User ID from signed URL
|
| 871 |
+
expires: Expiration timestamp
|
| 872 |
+
signature: URL signature for verification
|
| 873 |
+
file_type: Optional file type
|
| 874 |
+
inline: Whether to serve inline
|
| 875 |
+
size: Thumbnail size (for thumbnails)
|
| 876 |
+
quality: Stream quality (for streaming)
|
| 877 |
+
file_service: File service dependency
|
| 878 |
+
|
| 879 |
+
Returns:
|
| 880 |
+
StreamingResponse with file content
|
| 881 |
+
|
| 882 |
+
Raises:
|
| 883 |
+
HTTPException: If URL is invalid, expired, or file not found
|
| 884 |
+
"""
|
| 885 |
+
try:
|
| 886 |
+
# Verify signed URL
|
| 887 |
+
from ...utils.secure_url_generator import verify_url_signature
|
| 888 |
+
|
| 889 |
+
params = {
|
| 890 |
+
'user_id': user_id,
|
| 891 |
+
'expires': expires,
|
| 892 |
+
'signature': signature
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
if file_type:
|
| 896 |
+
params['file_type'] = file_type
|
| 897 |
+
if inline:
|
| 898 |
+
params['inline'] = inline
|
| 899 |
+
if size:
|
| 900 |
+
params['size'] = size
|
| 901 |
+
if quality:
|
| 902 |
+
params['quality'] = quality
|
| 903 |
+
|
| 904 |
+
verification_result = verify_url_signature(file_id, params)
|
| 905 |
+
|
| 906 |
+
if not verification_result['valid']:
|
| 907 |
+
logger.warning(
|
| 908 |
+
"Invalid signed URL access attempt",
|
| 909 |
+
file_id=file_id,
|
| 910 |
+
user_id=user_id,
|
| 911 |
+
error=verification_result['error']
|
| 912 |
+
)
|
| 913 |
+
raise HTTPException(
|
| 914 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 915 |
+
detail=f"Invalid URL: {verification_result['error']}"
|
| 916 |
+
)
|
| 917 |
+
|
| 918 |
+
# Get file metadata
|
| 919 |
+
file_metadata = await file_service.get_file_metadata(
|
| 920 |
+
file_id=file_id,
|
| 921 |
+
user_id=verification_result['user_id']
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
if not file_metadata:
|
| 925 |
+
raise HTTPException(
|
| 926 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 927 |
+
detail="File not found"
|
| 928 |
+
)
|
| 929 |
+
|
| 930 |
+
logger.info(
|
| 931 |
+
"Serving secure file access",
|
| 932 |
+
file_id=file_id,
|
| 933 |
+
user_id=verification_result['user_id'],
|
| 934 |
+
file_type=verification_result.get('file_type'),
|
| 935 |
+
inline=verification_result['inline']
|
| 936 |
+
)
|
| 937 |
+
|
| 938 |
+
# Track file access
|
| 939 |
+
from ...utils.file_cache import track_file_access
|
| 940 |
+
access_type = "download"
|
| 941 |
+
if verification_result.get('file_type') == "thumbnail":
|
| 942 |
+
access_type = "thumbnail"
|
| 943 |
+
elif verification_result.get('file_type') == "stream":
|
| 944 |
+
access_type = "stream"
|
| 945 |
+
|
| 946 |
+
await track_file_access(
|
| 947 |
+
file_id=file_id,
|
| 948 |
+
user_id=verification_result['user_id'],
|
| 949 |
+
access_type=access_type
|
| 950 |
+
)
|
| 951 |
+
|
| 952 |
+
# Serve file using enhanced file serving
|
| 953 |
+
from ...utils.file_serving import serve_file_secure
|
| 954 |
+
|
| 955 |
+
return await serve_file_secure(
|
| 956 |
+
file_path=file_metadata.file_path,
|
| 957 |
+
file_type=file_metadata.file_type,
|
| 958 |
+
original_filename=file_metadata.original_filename,
|
| 959 |
+
request=request,
|
| 960 |
+
inline=verification_result['inline'],
|
| 961 |
+
enable_caching=True,
|
| 962 |
+
user_id=verification_result['user_id']
|
| 963 |
+
)
|
| 964 |
+
|
| 965 |
+
except HTTPException:
|
| 966 |
+
raise
|
| 967 |
+
except Exception as e:
|
| 968 |
+
logger.error(
|
| 969 |
+
"Failed to serve secure file access",
|
| 970 |
+
file_id=file_id,
|
| 971 |
+
user_id=user_id,
|
| 972 |
+
error=str(e),
|
| 973 |
+
exc_info=True
|
| 974 |
+
)
|
| 975 |
+
raise HTTPException(
|
| 976 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 977 |
+
detail=f"Failed to serve file: {str(e)}"
|
| 978 |
+
)
|
src/app/api/v1/jobs.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Job management API endpoints.
|
| 3 |
+
|
| 4 |
+
This module implements REST API endpoints for job management operations,
|
| 5 |
+
including job listing, cancellation, deletion, and log retrieval.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Optional, Dict, Any, List
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
|
| 12 |
+
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
|
| 13 |
+
from redis.asyncio import Redis
|
| 14 |
+
|
| 15 |
+
from ...core.redis import get_redis, RedisKeyManager, redis_json_get, redis_json_set
|
| 16 |
+
from ...core.cache import cache_response, CacheConfig
|
| 17 |
+
from ...core.performance import performance_monitor
|
| 18 |
+
from ...models.job import (
|
| 19 |
+
JobListResponse, JobResponse, JobStatus, Job,
|
| 20 |
+
JobStatusResponse, JobType, JobPriority
|
| 21 |
+
)
|
| 22 |
+
from ...services.job_service import JobService
|
| 23 |
+
from ...api.dependencies import (
|
| 24 |
+
get_current_user, get_job_service, get_pagination_params,
|
| 25 |
+
get_job_filters, PaginationParams, JobFilters, validate_job_ownership
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
router = APIRouter(prefix="/jobs", tags=["jobs"])
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@router.get("", response_model=JobListResponse)
|
| 33 |
+
async def list_jobs(
|
| 34 |
+
request: Request,
|
| 35 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 36 |
+
pagination: PaginationParams = Depends(get_pagination_params),
|
| 37 |
+
filters: JobFilters = Depends(get_job_filters),
|
| 38 |
+
job_service: JobService = Depends(get_job_service)
|
| 39 |
+
) -> JobListResponse:
|
| 40 |
+
"""
|
| 41 |
+
List jobs for the current user with pagination and filtering.
|
| 42 |
+
|
| 43 |
+
This endpoint returns a paginated list of jobs belonging to the current user,
|
| 44 |
+
with optional filtering by status, type, priority, and creation date.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
current_user: Authenticated user information
|
| 48 |
+
pagination: Pagination parameters
|
| 49 |
+
filters: Job filtering parameters
|
| 50 |
+
job_service: Job service dependency
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
JobListResponse with paginated job list
|
| 54 |
+
|
| 55 |
+
Raises:
|
| 56 |
+
HTTPException: If job retrieval fails
|
| 57 |
+
"""
|
| 58 |
+
try:
|
| 59 |
+
user_id = current_user["user_info"]["id"]
|
| 60 |
+
|
| 61 |
+
logger.info(
|
| 62 |
+
"Listing jobs for user",
|
| 63 |
+
user_id=user_id,
|
| 64 |
+
page=pagination.page,
|
| 65 |
+
items_per_page=pagination.items_per_page,
|
| 66 |
+
filters=filters.to_dict()
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Get jobs from service
|
| 70 |
+
jobs_result = await job_service.get_jobs_paginated(
|
| 71 |
+
user_id=user_id,
|
| 72 |
+
limit=pagination.limit,
|
| 73 |
+
offset=pagination.offset,
|
| 74 |
+
filters=filters.to_dict()
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Convert jobs to response format
|
| 78 |
+
job_responses = []
|
| 79 |
+
for job in jobs_result["jobs"]:
|
| 80 |
+
job_responses.append(JobResponse(
|
| 81 |
+
job_id=job.id,
|
| 82 |
+
status=job.status,
|
| 83 |
+
progress=job.progress,
|
| 84 |
+
created_at=job.created_at,
|
| 85 |
+
estimated_completion=job.progress.estimated_completion
|
| 86 |
+
))
|
| 87 |
+
|
| 88 |
+
# Calculate pagination info
|
| 89 |
+
total_count = jobs_result["total_count"]
|
| 90 |
+
has_next = (pagination.offset + pagination.items_per_page) < total_count
|
| 91 |
+
has_previous = pagination.page > 1
|
| 92 |
+
|
| 93 |
+
logger.info(
|
| 94 |
+
"Retrieved jobs for user",
|
| 95 |
+
user_id=user_id,
|
| 96 |
+
job_count=len(job_responses),
|
| 97 |
+
total_count=total_count
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return JobListResponse(
|
| 101 |
+
jobs=job_responses,
|
| 102 |
+
total_count=total_count,
|
| 103 |
+
page=pagination.page,
|
| 104 |
+
items_per_page=pagination.items_per_page,
|
| 105 |
+
has_next=has_next,
|
| 106 |
+
has_previous=has_previous
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(
|
| 111 |
+
"Failed to list jobs",
|
| 112 |
+
user_id=current_user["user_info"]["id"],
|
| 113 |
+
error=str(e),
|
| 114 |
+
exc_info=True
|
| 115 |
+
)
|
| 116 |
+
raise HTTPException(
|
| 117 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 118 |
+
detail=f"Failed to retrieve jobs: {str(e)}"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@router.post("/{job_id}/cancel", response_model=Dict[str, Any])
|
| 123 |
+
async def cancel_job(
|
| 124 |
+
job_id: str,
|
| 125 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 126 |
+
job_service: JobService = Depends(get_job_service),
|
| 127 |
+
redis_client: Redis = Depends(get_redis)
|
| 128 |
+
) -> Dict[str, Any]:
|
| 129 |
+
"""
|
| 130 |
+
Cancel a specific job.
|
| 131 |
+
|
| 132 |
+
This endpoint attempts to cancel a job if it's in a cancellable state
|
| 133 |
+
(queued or processing). Completed or already cancelled jobs cannot be cancelled.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
job_id: Unique job identifier
|
| 137 |
+
current_user: Authenticated user information
|
| 138 |
+
job_service: Job service dependency
|
| 139 |
+
redis_client: Redis client dependency
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
Dict with cancellation status and message
|
| 143 |
+
|
| 144 |
+
Raises:
|
| 145 |
+
HTTPException: If job not found, access denied, or cancellation fails
|
| 146 |
+
"""
|
| 147 |
+
try:
|
| 148 |
+
# Get job from Redis
|
| 149 |
+
job_key = RedisKeyManager.job_key(job_id)
|
| 150 |
+
job_data = await redis_json_get(redis_client, job_key)
|
| 151 |
+
|
| 152 |
+
if not job_data:
|
| 153 |
+
raise HTTPException(
|
| 154 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 155 |
+
detail=f"Job {job_id} not found"
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
job = Job(**job_data)
|
| 159 |
+
|
| 160 |
+
# Verify user owns this job
|
| 161 |
+
validate_job_ownership(job.user_id, current_user)
|
| 162 |
+
|
| 163 |
+
# Check if job can be cancelled
|
| 164 |
+
if not job.can_be_cancelled:
|
| 165 |
+
raise HTTPException(
|
| 166 |
+
status_code=status.HTTP_409_CONFLICT,
|
| 167 |
+
detail=f"Job cannot be cancelled. Current status: {job.status}"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
logger.info(
|
| 171 |
+
"Cancelling job",
|
| 172 |
+
job_id=job_id,
|
| 173 |
+
current_status=job.status,
|
| 174 |
+
user_id=current_user["user_info"]["id"]
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# Cancel job using service
|
| 178 |
+
success = await job_service.cancel_job(job_id)
|
| 179 |
+
|
| 180 |
+
if not success:
|
| 181 |
+
raise HTTPException(
|
| 182 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 183 |
+
detail="Failed to cancel job"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
logger.info(
|
| 187 |
+
"Job cancelled successfully",
|
| 188 |
+
job_id=job_id,
|
| 189 |
+
user_id=current_user["user_info"]["id"]
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
return {
|
| 193 |
+
"success": True,
|
| 194 |
+
"message": f"Job {job_id} has been cancelled",
|
| 195 |
+
"job_id": job_id,
|
| 196 |
+
"previous_status": job.status,
|
| 197 |
+
"new_status": "cancelled",
|
| 198 |
+
"cancelled_at": datetime.utcnow().isoformat()
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
except HTTPException:
|
| 202 |
+
raise
|
| 203 |
+
except Exception as e:
|
| 204 |
+
logger.error(
|
| 205 |
+
"Failed to cancel job",
|
| 206 |
+
job_id=job_id,
|
| 207 |
+
user_id=current_user["user_info"]["id"],
|
| 208 |
+
error=str(e),
|
| 209 |
+
exc_info=True
|
| 210 |
+
)
|
| 211 |
+
raise HTTPException(
|
| 212 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 213 |
+
detail=f"Failed to cancel job: {str(e)}"
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
@router.delete("/{job_id}", response_model=Dict[str, Any])
|
| 218 |
+
async def delete_job(
|
| 219 |
+
job_id: str,
|
| 220 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 221 |
+
job_service: JobService = Depends(get_job_service),
|
| 222 |
+
redis_client: Redis = Depends(get_redis)
|
| 223 |
+
) -> Dict[str, Any]:
|
| 224 |
+
"""
|
| 225 |
+
Delete a specific job (soft delete).
|
| 226 |
+
|
| 227 |
+
This endpoint performs a soft delete on a job, marking it as deleted
|
| 228 |
+
but preserving the data for audit purposes. The job will no longer
|
| 229 |
+
appear in normal job listings.
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
job_id: Unique job identifier
|
| 233 |
+
current_user: Authenticated user information
|
| 234 |
+
job_service: Job service dependency
|
| 235 |
+
redis_client: Redis client dependency
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
Dict with deletion status and message
|
| 239 |
+
|
| 240 |
+
Raises:
|
| 241 |
+
HTTPException: If job not found, access denied, or deletion fails
|
| 242 |
+
"""
|
| 243 |
+
try:
|
| 244 |
+
# Get job from Redis
|
| 245 |
+
job_key = RedisKeyManager.job_key(job_id)
|
| 246 |
+
job_data = await redis_json_get(redis_client, job_key)
|
| 247 |
+
|
| 248 |
+
if not job_data:
|
| 249 |
+
raise HTTPException(
|
| 250 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 251 |
+
detail=f"Job {job_id} not found"
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
job = Job(**job_data)
|
| 255 |
+
|
| 256 |
+
# Verify user owns this job
|
| 257 |
+
validate_job_ownership(job.user_id, current_user)
|
| 258 |
+
|
| 259 |
+
# Check if job is already deleted
|
| 260 |
+
if job.is_deleted:
|
| 261 |
+
raise HTTPException(
|
| 262 |
+
status_code=status.HTTP_409_CONFLICT,
|
| 263 |
+
detail="Job is already deleted"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
logger.info(
|
| 267 |
+
"Deleting job",
|
| 268 |
+
job_id=job_id,
|
| 269 |
+
status=job.status,
|
| 270 |
+
user_id=current_user["user_info"]["id"]
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# Cancel job if it's still active
|
| 274 |
+
if job.can_be_cancelled:
|
| 275 |
+
await job_service.cancel_job(job_id)
|
| 276 |
+
|
| 277 |
+
# Perform soft delete
|
| 278 |
+
success = await job_service.soft_delete_job(job_id)
|
| 279 |
+
|
| 280 |
+
if not success:
|
| 281 |
+
raise HTTPException(
|
| 282 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 283 |
+
detail="Failed to delete job"
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
# Remove from user's job index
|
| 287 |
+
user_jobs_key = RedisKeyManager.user_jobs_key(current_user["user_info"]["id"])
|
| 288 |
+
await redis_client.srem(user_jobs_key, job_id)
|
| 289 |
+
|
| 290 |
+
# Clean up related data (video metadata, cache entries)
|
| 291 |
+
await job_service.cleanup_job_data(job_id)
|
| 292 |
+
|
| 293 |
+
logger.info(
|
| 294 |
+
"Job deleted successfully",
|
| 295 |
+
job_id=job_id,
|
| 296 |
+
user_id=current_user["user_info"]["id"]
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
return {
|
| 300 |
+
"success": True,
|
| 301 |
+
"message": f"Job {job_id} has been deleted",
|
| 302 |
+
"job_id": job_id,
|
| 303 |
+
"deleted_at": datetime.utcnow().isoformat()
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
except HTTPException:
|
| 307 |
+
raise
|
| 308 |
+
except Exception as e:
|
| 309 |
+
logger.error(
|
| 310 |
+
"Failed to delete job",
|
| 311 |
+
job_id=job_id,
|
| 312 |
+
user_id=current_user["user_info"]["id"],
|
| 313 |
+
error=str(e),
|
| 314 |
+
exc_info=True
|
| 315 |
+
)
|
| 316 |
+
raise HTTPException(
|
| 317 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 318 |
+
detail=f"Failed to delete job: {str(e)}"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
@router.get("/{job_id}/logs", response_model=Dict[str, Any])
|
| 323 |
+
async def get_job_logs(
|
| 324 |
+
job_id: str,
|
| 325 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 326 |
+
limit: int = Query(100, ge=1, le=1000, description="Maximum number of log entries"),
|
| 327 |
+
offset: int = Query(0, ge=0, description="Number of log entries to skip"),
|
| 328 |
+
level: Optional[str] = Query(None, description="Filter by log level (DEBUG, INFO, WARNING, ERROR)"),
|
| 329 |
+
redis_client: Redis = Depends(get_redis)
|
| 330 |
+
) -> Dict[str, Any]:
|
| 331 |
+
"""
|
| 332 |
+
Get processing logs for a specific job.
|
| 333 |
+
|
| 334 |
+
This endpoint returns the processing logs for a job, which can be useful
|
| 335 |
+
for debugging failed jobs or monitoring progress in detail.
|
| 336 |
+
|
| 337 |
+
Args:
|
| 338 |
+
job_id: Unique job identifier
|
| 339 |
+
current_user: Authenticated user information
|
| 340 |
+
limit: Maximum number of log entries to return
|
| 341 |
+
offset: Number of log entries to skip
|
| 342 |
+
level: Filter logs by level
|
| 343 |
+
redis_client: Redis client dependency
|
| 344 |
+
|
| 345 |
+
Returns:
|
| 346 |
+
Dict with job logs and metadata
|
| 347 |
+
|
| 348 |
+
Raises:
|
| 349 |
+
HTTPException: If job not found or access denied
|
| 350 |
+
"""
|
| 351 |
+
try:
|
| 352 |
+
# Get job from Redis
|
| 353 |
+
job_key = RedisKeyManager.job_key(job_id)
|
| 354 |
+
job_data = await redis_json_get(redis_client, job_key)
|
| 355 |
+
|
| 356 |
+
if not job_data:
|
| 357 |
+
raise HTTPException(
|
| 358 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 359 |
+
detail=f"Job {job_id} not found"
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
job = Job(**job_data)
|
| 363 |
+
|
| 364 |
+
# Verify user owns this job
|
| 365 |
+
validate_job_ownership(job.user_id, current_user)
|
| 366 |
+
|
| 367 |
+
logger.info(
|
| 368 |
+
"Retrieving job logs",
|
| 369 |
+
job_id=job_id,
|
| 370 |
+
limit=limit,
|
| 371 |
+
offset=offset,
|
| 372 |
+
level=level,
|
| 373 |
+
user_id=current_user["user_info"]["id"]
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
# Get logs from Redis (stored as a list)
|
| 377 |
+
logs_key = f"job_logs:{job_id}"
|
| 378 |
+
|
| 379 |
+
# Get total log count
|
| 380 |
+
total_logs = await redis_client.llen(logs_key)
|
| 381 |
+
|
| 382 |
+
# Get log entries with pagination
|
| 383 |
+
log_entries = []
|
| 384 |
+
if total_logs > 0:
|
| 385 |
+
# Redis lists are 0-indexed, get range with offset and limit
|
| 386 |
+
end_index = offset + limit - 1
|
| 387 |
+
if end_index >= total_logs:
|
| 388 |
+
end_index = total_logs - 1
|
| 389 |
+
|
| 390 |
+
if offset < total_logs:
|
| 391 |
+
raw_logs = await redis_client.lrange(logs_key, offset, end_index)
|
| 392 |
+
|
| 393 |
+
# Parse and filter logs
|
| 394 |
+
for log_entry in raw_logs:
|
| 395 |
+
try:
|
| 396 |
+
import json
|
| 397 |
+
log_data = json.loads(log_entry)
|
| 398 |
+
|
| 399 |
+
# Filter by level if specified
|
| 400 |
+
if level and log_data.get("level", "").upper() != level.upper():
|
| 401 |
+
continue
|
| 402 |
+
|
| 403 |
+
log_entries.append(log_data)
|
| 404 |
+
|
| 405 |
+
except json.JSONDecodeError:
|
| 406 |
+
# Handle plain text logs
|
| 407 |
+
log_entries.append({
|
| 408 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 409 |
+
"level": "INFO",
|
| 410 |
+
"message": log_entry,
|
| 411 |
+
"source": "system"
|
| 412 |
+
})
|
| 413 |
+
|
| 414 |
+
# Calculate pagination info
|
| 415 |
+
has_more = (offset + limit) < total_logs
|
| 416 |
+
|
| 417 |
+
logger.info(
|
| 418 |
+
"Retrieved job logs",
|
| 419 |
+
job_id=job_id,
|
| 420 |
+
log_count=len(log_entries),
|
| 421 |
+
total_logs=total_logs,
|
| 422 |
+
user_id=current_user["user_info"]["id"]
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
return {
|
| 426 |
+
"job_id": job_id,
|
| 427 |
+
"logs": log_entries,
|
| 428 |
+
"pagination": {
|
| 429 |
+
"offset": offset,
|
| 430 |
+
"limit": limit,
|
| 431 |
+
"total_count": total_logs,
|
| 432 |
+
"returned_count": len(log_entries),
|
| 433 |
+
"has_more": has_more
|
| 434 |
+
},
|
| 435 |
+
"filters": {
|
| 436 |
+
"level": level
|
| 437 |
+
},
|
| 438 |
+
"job_info": {
|
| 439 |
+
"status": job.status,
|
| 440 |
+
"created_at": job.created_at.isoformat(),
|
| 441 |
+
"updated_at": job.updated_at.isoformat(),
|
| 442 |
+
"progress": job.progress.percentage
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
except HTTPException:
|
| 447 |
+
raise
|
| 448 |
+
except Exception as e:
|
| 449 |
+
logger.error(
|
| 450 |
+
"Failed to get job logs",
|
| 451 |
+
job_id=job_id,
|
| 452 |
+
user_id=current_user["user_info"]["id"],
|
| 453 |
+
error=str(e),
|
| 454 |
+
exc_info=True
|
| 455 |
+
)
|
| 456 |
+
raise HTTPException(
|
| 457 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 458 |
+
detail=f"Failed to retrieve job logs: {str(e)}"
|
| 459 |
+
)
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
@router.get("/{job_id}", response_model=JobStatusResponse)
|
| 463 |
+
async def get_job_details(
|
| 464 |
+
job_id: str,
|
| 465 |
+
current_user: Dict[str, Any] = Depends(get_current_user),
|
| 466 |
+
redis_client: Redis = Depends(get_redis)
|
| 467 |
+
) -> JobStatusResponse:
|
| 468 |
+
"""
|
| 469 |
+
Get detailed information about a specific job.
|
| 470 |
+
|
| 471 |
+
This endpoint returns comprehensive job information including status,
|
| 472 |
+
progress, configuration, metrics, and error details if applicable.
|
| 473 |
+
|
| 474 |
+
Args:
|
| 475 |
+
job_id: Unique job identifier
|
| 476 |
+
current_user: Authenticated user information
|
| 477 |
+
redis_client: Redis client dependency
|
| 478 |
+
|
| 479 |
+
Returns:
|
| 480 |
+
JobStatusResponse with detailed job information
|
| 481 |
+
|
| 482 |
+
Raises:
|
| 483 |
+
HTTPException: If job not found or access denied
|
| 484 |
+
"""
|
| 485 |
+
try:
|
| 486 |
+
# Get job from Redis
|
| 487 |
+
job_key = RedisKeyManager.job_key(job_id)
|
| 488 |
+
job_data = await redis_json_get(redis_client, job_key)
|
| 489 |
+
|
| 490 |
+
if not job_data:
|
| 491 |
+
raise HTTPException(
|
| 492 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 493 |
+
detail=f"Job {job_id} not found"
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
job = Job(**job_data)
|
| 497 |
+
|
| 498 |
+
# Verify user owns this job
|
| 499 |
+
validate_job_ownership(job.user_id, current_user)
|
| 500 |
+
|
| 501 |
+
logger.info(
|
| 502 |
+
"Retrieved job details",
|
| 503 |
+
job_id=job_id,
|
| 504 |
+
status=job.status,
|
| 505 |
+
user_id=current_user["user_info"]["id"]
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
return JobStatusResponse(
|
| 509 |
+
job_id=job.id,
|
| 510 |
+
status=job.status,
|
| 511 |
+
progress=job.progress,
|
| 512 |
+
error=job.error,
|
| 513 |
+
metrics=job.metrics,
|
| 514 |
+
created_at=job.created_at,
|
| 515 |
+
updated_at=job.updated_at,
|
| 516 |
+
completed_at=job.completed_at
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
except HTTPException:
|
| 520 |
+
raise
|
| 521 |
+
except Exception as e:
|
| 522 |
+
logger.error(
|
| 523 |
+
"Failed to get job details",
|
| 524 |
+
job_id=job_id,
|
| 525 |
+
user_id=current_user["user_info"]["id"],
|
| 526 |
+
error=str(e),
|
| 527 |
+
exc_info=True
|
| 528 |
+
)
|
| 529 |
+
raise HTTPException(
|
| 530 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 531 |
+
detail=f"Failed to retrieve job details: {str(e)}"
|
| 532 |
+
)
|