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

1from dotenv import load_dotenv 

2 

3load_dotenv() # Load .env 

4 

5import logging.config 

6import os 

7import re 

8 

9import yaml 

10from pydantic_settings import BaseSettings 

11 

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"}) 

17 

18 

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). 

21 

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) 

32 

33 

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" 

39 

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 

47 

48 # OpenTelemetry settings 

49 OTEL_ENABLE: bool = False 

50 OTEL_EXPORTER_OTLP_ENDPOINT: str = "http://alloy:4318" 

51 

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 

62 

63 # Redis settings 

64 REDIS_URL: str 

65 

66 # CORS settings 

67 HOSTNAME: str 

68 BACKEND_PORT: str 

69 FRONTEND_PORT: str 

70 

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 

77 

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 

82 

83 # Session settings 

84 SESSION_EXPIRE_MINUTES: int = 10080 # 7 days 

85 CSRF_TOKEN_EXPIRE_MINUTES: int = 30 

86 

87 # Cookie settings 

88 COOKIE_SECURE: bool = SSL_ENABLE 

89 COOKIE_HTTPONLY: bool = True 

90 COOKIE_SAMESITE: str = "lax" # "strict", "lax", "none" 

91 

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 = "" 

98 

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()} 

105 

106 # Registration settings 

107 REGISTRATION_ENABLE: bool = True 

108 

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" 

118 

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 

133 

134 

135# Create a settings instance to be imported elsewhere 

136settings = Settings() 

137 

138 

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)