Files
TieMeasureFlow/server/config.py
T
Adriano 86df67f2e5 perf: scale workers + per-tablet rate limiting for 20 concurrent users
The default 2-worker gunicorn could only serve 2 concurrent tablet requests,
queueing the rest, and the rate limiter saw every tablet as the same Nginx
container IP, so 20 users would have collectively burned through the
100 req/min general bucket.

- gunicorn: 5 workers x 4 gthread, --forwarded-allow-ips=*, access log
- uvicorn: 4 workers, --proxy-headers, --forwarded-allow-ips=*
- RateLimitMiddleware: resolve real client IP from
  X-Forwarded-For -> X-Real-IP -> request.client.host
- Bump rate_limit_general 100 -> 300 req/min/IP (per tablet now)
- Flask: ProxyFix(x_for=1, x_proto=1, x_host=1) so request.remote_addr
  is the tablet IP, not the Nginx IP
- APIClient: forward X-Forwarded-For + X-Real-IP to FastAPI for both
  JSON and multipart/files calls; safe no-op outside request context
- 12 new tests (7 server + 5 client) covering header precedence,
  forwarding behavior and ProxyFix install

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:07:43 +02:00

59 lines
1.7 KiB
Python

"""TieMeasureFlow Server Configuration."""
from pathlib import Path
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# Database
db_host: str = "localhost"
db_port: int = 3306
db_name: str = "tiemeasureflow"
db_user: str = "tmflow"
db_password: str = "change_me_in_production"
# Server
server_host: str = "0.0.0.0"
server_port: int = 8000
server_secret_key: str = "change-this-to-a-random-secret-key"
server_cors_origins: str = "http://localhost:5000"
# File Storage
upload_dir: str = "uploads"
max_upload_size_mb: int = 50
# Rate Limiting (requests per minute, per real client IP)
rate_limit_login: int = 5
rate_limit_general: int = 300
# SSL (Production)
ssl_certfile: str | None = None
ssl_keyfile: str | None = None
# Setup page (empty = disabled)
setup_password: str | None = None
@property
def database_url(self) -> str:
"""Async MySQL connection string."""
return (
f"mysql+asyncmy://{self.db_user}:{self.db_password}"
f"@{self.db_host}:{self.db_port}/{self.db_name}"
)
@property
def cors_origins(self) -> list[str]:
"""Parse CORS origins from comma-separated string."""
return [origin.strip() for origin in self.server_cors_origins.split(",")]
@property
def upload_path(self) -> Path:
"""Absolute path to upload directory."""
return Path(__file__).parent / self.upload_dir
model_config = {"env_file": "../.env", "env_file_encoding": "utf-8", "extra": "ignore"}
settings = Settings()