Coverage for core/config.py: 98.90%
91 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-03 15:30 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-03 15:30 +0000
1from dotenv import load_dotenv
3load_dotenv() # Load .env
5import logging.config
6import os
7import re
9import yaml
10from pydantic_settings import BaseSettings
12# Docs / health probes — skip logging, rate limit, and tracing.
13# Not an env setting: these paths are part of the app, not deployment.
14SKIP_PATHS: frozenset[str] = frozenset({"/", "/docs", "/redoc", "/openapi.json", "/healthz"})
15# CORS preflight — skip logging, rate limit, and tracing (URL exclude cannot filter method).
16SKIP_METHODS: frozenset[str] = frozenset({"OPTIONS"})
19def otel_excluded_urls(paths: frozenset[str] = SKIP_PATHS) -> str:
20 """Regexes matched with search() against the full URL (e.g. http://host:5000/healthz).
22 Anchored path-only patterns like ^/healthz$ never match, so Docker healthchecks
23 would still create server spans and show up in p95.
24 """
25 patterns: list[str] = []
26 for path in sorted(paths):
27 if path == "/":
28 patterns.append(r"https?://[^/?]+/?$")
29 else:
30 patterns.append(re.escape(path))
31 return ",".join(patterns)
34class Settings(BaseSettings):
35 # Project settings
36 PROJECT_NAME: str = "Backend API Docs"
37 PROJECT_VERSION: str = "1.0.0"
38 PROJECT_DESCRIPTION: str = "Backend API Docs"
40 # Basic settings
41 DEBUG_MODE: bool = True
42 LOG_LEVEL: str = "INFO"
43 LOG_LOCAL_RETENTION_DAYS: int = 7
44 LOG_HTTP_BODY: bool = False
45 LOG_HTTP_BODY_MAX_BYTES: int = 8192
46 SSL_ENABLE: bool = False
48 # OpenTelemetry settings
49 OTEL_ENABLE: bool = False
50 OTEL_EXPORTER_OTLP_ENDPOINT: str = "http://alloy:4318"
52 # Database settings
53 DATABASE_URL: str
54 DATABASE_URL_TEST: str
55 DB_POOL_SIZE: int = 10
56 DB_MAX_OVERFLOW: int = 20
57 DB_POOL_TIMEOUT: int = 30
58 DB_POOL_RECYCLE: int = 3600
59 DB_CONNECT_TIMEOUT: int = 60
60 DB_READ_TIMEOUT: int = 30
61 DB_WRITE_TIMEOUT: int = 30
63 # Redis settings
64 REDIS_URL: str
66 # CORS settings
67 HOSTNAME: str
68 BACKEND_PORT: str
69 FRONTEND_PORT: str
71 # JWT settings
72 SECRET_KEY: str
73 ALGORITHM: str = "HS256"
74 ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 1 day
75 PASSWORD_RESET_TOKEN_EXPIRE_MINUTES: int = 30 # 30 minutes
76 PASSWORD_RESET_EMAIL_COOLDOWN_SECONDS: int = 60 # 1 minute
78 # Email verification settings
79 EMAIL_VERIFICATION_ENABLE: bool = False
80 EMAIL_VERIFICATION_TOKEN_EXPIRE_MINUTES: int = 30 # 30 minutes
81 EMAIL_VERIFICATION_COOLDOWN_SECONDS: int = 60 # 1 minute
83 # Session settings
84 SESSION_EXPIRE_MINUTES: int = 10080 # 7 days
85 CSRF_TOKEN_EXPIRE_MINUTES: int = 30
87 # Cookie settings
88 COOKIE_SECURE: bool = SSL_ENABLE
89 COOKIE_HTTPONLY: bool = True
90 COOKIE_SAMESITE: str = "lax" # "strict", "lax", "none"
92 # Security settings
93 PASSWORD_MIN_LENGTH: int = 6
94 RATE_LIMIT: int = 200
95 RATE_LIMIT_WINDOW_SECONDS: int = 300 # 5 minutes
96 BLOCK_TIME_SECONDS: int = 600 # 10 minutes
97 RATE_LIMIT_WHITELIST: str = ""
99 @property
100 def rate_limit_whitelist_ips(self) -> set[str]:
101 raw = (self.RATE_LIMIT_WHITELIST or "").strip()
102 if not raw:
103 return set()
104 return {ip.strip() for ip in raw.split(",") if ip.strip()}
106 # Registration settings
107 REGISTRATION_ENABLE: bool = True
109 # SMTP setting
110 SMTP_ENABLE: bool = False
111 SMTP_HOST: str = "smtp.gmail.com"
112 SMTP_PORT: int = 587
113 SMTP_USERNAME: str = ""
114 SMTP_PASSWORD: str = ""
115 SMTP_FROM_EMAIL: str = ""
116 SMTP_FROM_NAME: str = "Docker Fullstack Template"
117 SMTP_ENCRYPTION: str = "tls"
119 # Default admin user settings
120 DEFAULT_ADMIN_EMAIL: str = "admin@example.com"
121 DEFAULT_ADMIN_PASSWORD: str = "admin123"
122 DEFAULT_ADMIN_FIRST_NAME: str = "Admin"
123 DEFAULT_ADMIN_LAST_NAME: str = "User"
124 DEFAULT_ADMIN_PHONE: str = "0000000000"
125 # System role with full access bypass (not editable via roles UI/API)
126 DEFAULT_SUPER_ADMIN_ROLE: str = "super-admin"
127 DEFAULT_SUPER_ADMIN_LEVEL: int = 100
128 DEFAULT_USER_ROLE_LEVEL: int = 1
129 # Custom roles created via API must stay below the system super-admin level
130 MAX_CUSTOM_ROLE_LEVEL: int = 99
131 # When false, users with the system super-admin role are hidden from user list for everyone
132 SHOW_SUPER_ADMIN: bool = False
135# Create a settings instance to be imported elsewhere
136settings = Settings()
139def setup_logging(yaml_path="logging_config.yaml"):
140 os.makedirs("logs", exist_ok=True)
141 with open(yaml_path) as f:
142 config = yaml.safe_load(f)
143 # Override the root logger or specified logger's level with LOG_LEVEL from environment
144 log_level = settings.LOG_LEVEL
145 if "root" in config:
146 config["root"]["level"] = log_level
147 # If there are multiple loggers, override their levels as well
148 if "loggers" in config:
149 for logger in config["loggers"].values():
150 logger["level"] = log_level
151 if "handlers" in config and "file" in config["handlers"]:
152 config["handlers"]["file"]["backupCount"] = settings.LOG_LOCAL_RETENTION_DAYS
153 logging.config.dictConfig(config)