scaffold: project skeleton, schema, healthz/readyz, CI
Initial project structure for neuronetz-gateway per scope-docs/SPEC.md: - Python 3.12 / FastAPI / SQLAlchemy 2.0 (async) / Redis / Postgres stack managed by uv. Multi-stage non-root Dockerfile, prod + dev compose files (ollama service is NEVER published in either), Caddyfile + systemd unit, justfile, GitHub Actions CI (ruff, mypy --strict, pytest, bandit, pip-audit). - Pydantic-Settings config covering every env var from SPEC §7, including the MODEL_DISCOVERY_* keys for the dynamic-discovery feature (§4.6). - Alembic 0001_initial creates the full gateway schema (8 tables, 3 enums, notify_key_revoked() trigger), incl. allow_all_models on tenant_limits and key_limits for the per-tenant auto-grant toggle. - Working /healthz, /readyz (fail-closed when deps unreachable), and a Prometheus /metrics stub. Sanitizing error handlers that attach X-Request-ID to every response and never leak upstream internals. - SPEC + AGENT_PROMPT included under scope-docs/ (source of truth).
This commit is contained in:
86
src/neuronetz_gateway/config.py
Normal file
86
src/neuronetz_gateway/config.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Application configuration via Pydantic Settings v2.
|
||||
|
||||
Reads every environment variable documented in SPEC §7 with the documented
|
||||
defaults. Boot fails loudly (ValidationError) on invalid config.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Gateway runtime configuration. All fields map to SPEC §7 env vars."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
# --- Service ---
|
||||
gateway_bind_host: str = Field(default="0.0.0.0") # noqa: S104 - bind-all is intended in container
|
||||
gateway_bind_port: int = Field(default=8080)
|
||||
gateway_log_level: str = Field(default="INFO")
|
||||
gateway_log_format: str = Field(default="json") # json|console
|
||||
gateway_request_id_header: str = Field(default="X-Request-ID")
|
||||
gateway_trusted_proxies: str = Field(default="127.0.0.1,caddy")
|
||||
|
||||
# --- Upstream (Ollama) ---
|
||||
ollama_base_url: str = Field(default="http://ollama:11434")
|
||||
ollama_connect_timeout_s: int = Field(default=5)
|
||||
ollama_read_timeout_s: int = Field(default=600)
|
||||
ollama_max_connections: int = Field(default=64)
|
||||
|
||||
# --- Model discovery (SPEC §4.6) ---
|
||||
model_discovery_refresh_s: int = Field(default=60)
|
||||
model_discovery_cache_ttl_s: int = Field(default=120)
|
||||
|
||||
# --- Database ---
|
||||
database_url: str = Field(
|
||||
default="postgresql+asyncpg://gateway:gateway@postgres:5432/neuronetz",
|
||||
)
|
||||
database_pool_size: int = Field(default=10)
|
||||
database_pool_overflow: int = Field(default=20)
|
||||
|
||||
# --- Redis ---
|
||||
redis_url: str = Field(default="redis://redis:6379/0")
|
||||
redis_key_cache_ttl_s: int = Field(default=60)
|
||||
|
||||
# --- Limits ---
|
||||
default_rpm: int = Field(default=60)
|
||||
default_tpm: int = Field(default=100_000)
|
||||
default_concurrent: int = Field(default=8)
|
||||
max_request_body_bytes: int = Field(default=262_144)
|
||||
max_num_predict: int = Field(default=4096)
|
||||
|
||||
# --- Security ---
|
||||
argon2_time_cost: int = Field(default=3)
|
||||
argon2_memory_cost_kib: int = Field(default=65_536)
|
||||
argon2_parallelism: int = Field(default=4)
|
||||
auth_failure_rate_limit_per_ip_per_min: int = Field(default=20)
|
||||
|
||||
# --- Audit ---
|
||||
audit_buffer_size: int = Field(default=1000)
|
||||
prompt_log_default_retention_days: int = Field(default=30)
|
||||
audit_log_default_retention_days: int = Field(default=365)
|
||||
|
||||
# --- Playground / docs (prod-safe defaults: both OFF) ---
|
||||
playground_enabled: bool = Field(default=False)
|
||||
playground_file: str = Field(default="/app/playground/index.html")
|
||||
docs_enabled: bool = Field(default=False)
|
||||
|
||||
@property
|
||||
def trusted_proxies_list(self) -> list[str]:
|
||||
"""Parse the comma-separated trusted-proxy list into individual hosts."""
|
||||
return [p.strip() for p in self.gateway_trusted_proxies.split(",") if p.strip()]
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
"""Return a cached Settings instance, constructed from the environment."""
|
||||
return Settings()
|
||||
Reference in New Issue
Block a user