Coverage for app / config.py: 86%

90 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-06 04:49 +0000

1""" 

2Configuration module for the Flask application managing environment-specific settings. 

3 

4This module handles loading configuration settings from environment variables and provides 

5different configuration classes for development, production, and testing environments. 

6It includes settings for: 

7- Database connections (MySQL/SQLAlchemy) 

8- Security configurations (passwords, cookies, sessions) 

9- Email settings 

10- Logging configurations 

11- API integrations 

12""" 

13import logging 

14import os 

15from pathlib import Path 

16from urllib.parse import quote_plus 

17 

18from dotenv import load_dotenv 

19 

20# Calculate the path to where the SQL files are 

21PROJECT_ROOT = Path(__file__).resolve().parent.parent # Navigate to the project root 

22INTEGRATION_DIR = PROJECT_ROOT / "tests" / "integration" 

23 

24# Determine which environment file to load 

25env = os.getenv("FLASK_ENV", "development").lower() # Default to "development" 

26dotenv_path = INTEGRATION_DIR / ".env.testing" if env in {"testing"} else PROJECT_ROOT / ".env" 

27 

28# Load the appropriate .env file 

29load_dotenv(str(dotenv_path)) 

30 

31 

32# pylint: disable=too-few-public-methods 

33class Config: 

34 """Base configuration with default settings.""" 

35 SECRET_KEY = os.getenv("SECRET_KEY", "default-secret-key") 

36 WTF_CSRF_ENABLED = True 

37 WTF_CSRF_SECRET_KEY = SECRET_KEY 

38 WTF_CSRF_TIME_LIMIT = 3600 # CSRF token expiration in seconds 

39 

40 RDS_HOSTNAME = os.getenv("RDS_HOSTNAME") 

41 RDS_PORT = os.getenv("RDS_PORT", "3306") 

42 RDS_DB_NAME = os.getenv("RDS_DB_NAME", "readinglist") 

43 RDS_USERNAME = os.getenv("RDS_USERNAME", "readinglist") 

44 RDS_PASSWORD = os.getenv("RDS_PASSWORD") 

45 

46 SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoid overhead of tracking 

47 

48 # Add any other app-wide default configurations here 

49 DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t", "yes") 

50 

51 # Flask-Mailman settings 

52 MAIL_SERVER = os.getenv("MAIL_SERVER") 

53 MAIL_PORT = os.getenv("MAIL_PORT") 

54 MAIL_USE_TLS = os.getenv("MAIL_USE_TLS") 

55 MAIL_USERNAME = os.getenv("MAIL_USERNAME") 

56 MAIL_PASSWORD = os.getenv("MAIL_PASSWORD") 

57 

58 # Generate a good salt for password hashing using: secrets.SystemRandom().getrandbits(128) 

59 SECURITY_PASSWORD_SALT = os.environ.get("SECURITY_PASSWORD_SALT") 

60 

61 # API key for ASIN service 

62 ASIN_DATA_API_KEY = os.environ.get("ASIN_DATA_API_KEY") 

63 ASIN_DATA_API_URL = os.environ.get("ASIN_DATA_API_URL", 'https://api.asindataapi.com/request') 

64 

65 # have session and remember cookie be samesite (flask/flask_login) 

66 REMEMBER_COOKIE_SAMESITE = "Lax" 

67 SESSION_COOKIE_SAMESITE = "Lax" 

68 SESSION_COOKIE_HTTPONLY = True 

69 REMEMBER_COOKIE_HTTPONLY = True 

70 SESSION_COOKIE_SECURE = True # Ensures cookies are sent only over HTTPS 

71 REMEMBER_COOKIE_SECURE = True # For "remember me" functionality 

72 

73 PERMANENT_SESSION_LIFETIME = 3600 # 1 hour 

74 SECURITY_TOKEN_MAX_AGE = 3600 # 1 hour for security tokens 

75 

76 SECURITY_USERNAME_ENABLE = False # keep it simple, just email 

77 SECURITY_USE_REGISTER_V2 = True 

78 SECURITY_REGISTERABLE = False # we provide our own register view 

79 SECURITY_POST_REGISTER_VIEW = "/admin/user/" 

80 

81 SECURITY_EMAIL_SENDER = os.getenv("SECURITY_EMAIL_SENDER") 

82 

83 SECURITY_CONFIRMABLE = True 

84 SECURITY_RECOVERABLE = True 

85 SECURITY_PASSWORD_HISTORY = 5 

86 SECURITY_RESET_PASSWORD_WITHIN = '1 hours' # nosec B105, noqa: dodgy:password 

87 

88 FLASK_ADMIN_SWATCH = "sandstone" 

89 

90 RATELIMIT_ENABLED = True 

91 

92 # Logging configuration - override in specific environments 

93 LOGGING_LEVEL = logging.INFO # Default logging level 

94 

95 CACHE_TYPE = "SimpleCache" # This configures an in-memory cache 

96 CACHE_DEFAULT_TIMEOUT = 300 # Values are cached for 300 seconds (5 minutes) by default 

97 

98 @classmethod 

99 def configure_logging(cls): 

100 """ 

101 Configures logging for the application. 

102 

103 This method sets up global logging with the specified logging level. Additionally, 

104 SQLAlchemy-specific logging is configured to the same level. 

105 """ 

106 # Global logging setup 

107 logging.basicConfig(level=cls.LOGGING_LEVEL) # Set the logging level dynamically 

108 

109 # Configure SQLAlchemy specific logging 

110 logging.getLogger("sqlalchemy.engine").setLevel(cls.LOGGING_LEVEL) 

111 

112 

113class DevelopmentConfig(Config): 

114 """Development-specific configuration.""" 

115 DEBUG = True 

116 LOGGING_LEVEL = logging.INFO 

117 SQLALCHEMY_ECHO = False # Log SQL queries for debugging 

118 SQLALCHEMY_DATABASE_URI = (f"mysql+pymysql://{Config.RDS_USERNAME}:" + 

119 f"{quote_plus(str(Config.RDS_PASSWORD))}@{Config.RDS_HOSTNAME}:" + 

120 f"{Config.RDS_PORT}/{Config.RDS_DB_NAME}") 

121 TEMPLATES_AUTO_RELOAD = True 

122 

123 

124class ProductionConfig(Config): 

125 """Production-specific configuration.""" 

126 DEBUG = False 

127 LOGGING_LEVEL = logging.WARNING 

128 SQLALCHEMY_ECHO = False 

129 SQLALCHEMY_DATABASE_URI = (f"mysql+pymysql://{Config.RDS_USERNAME}:" + 

130 f"{quote_plus(str(Config.RDS_PASSWORD))}@{Config.RDS_HOSTNAME}:" + 

131 f"{Config.RDS_PORT}/{Config.RDS_DB_NAME}") 

132 cookie_domain = os.getenv("COOKIE_DOMAIN") 

133 if cookie_domain: 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true

134 REMEMBER_COOKIE_DOMAIN = cookie_domain 

135 SESSION_COOKIE_DOMAIN = cookie_domain 

136 

137 

138class TestingConfig(Config): 

139 """Testing-specific configuration.""" 

140 TESTING = True 

141 DEBUG = True 

142 LOGGING_LEVEL = logging.WARNING 

143 RATELIMIT_ENABLED = False # Disable rate-limiting in testing 

144 SERVER_NAME = '0.0.0.0:8000' 

145 SQLALCHEMY_DATABASE_URI = (f"mysql+pymysql://{Config.RDS_USERNAME}:" + 

146 f"{quote_plus(str(Config.RDS_PASSWORD))}@{Config.RDS_HOSTNAME}:" + 

147 f"{Config.RDS_PORT}/{Config.RDS_DB_NAME}") 

148 

149 # pylint: disable=invalid-name 

150 def __init__(self): 

151 super().__init__() 

152 

153 # Workaround for running tests in a container locally under act 

154 # see(https://github.com/nektos/act) 

155 if os.getenv("ACT") and os.getenv("RDS_HOSTNAME") == "localhost": 

156 self.RDS_HOSTNAME = "host.docker.internal" 

157 self.SQLALCHEMY_DATABASE_URI = (f"mysql+pymysql://{Config.RDS_USERNAME}:" + 

158 f"{quote_plus(str(Config.RDS_PASSWORD))}" + 

159 f"@{self.RDS_HOSTNAME}:" + 

160 f"{Config.RDS_PORT}/{Config.RDS_DB_NAME}") 

161 if os.getenv("ACT") and os.getenv("mail_server") == "localhost": 

162 self.MAIL_SERVER = "host.docker.internal" 

163 

164 

165# Automatically configure logging for the chosen environment 

166def configure_app_logging(environment="development"): 

167 """ 

168 Configures the application logging based on the given environment. 

169 

170 This function retrieves a configuration class corresponding to the provided 

171 environment and applies its logging configuration. It is used to ensure that 

172 the application's logging adheres to the desired environment-specific 

173 settings. 

174 """ 

175 config_class = _config_by_name[environment] 

176 config_class.configure_logging() 

177 

178 

179# A dictionary to easily map environment modes to configuration classes 

180_config_by_name = { 

181 "development": DevelopmentConfig, 

182 "production": ProductionConfig, 

183 "testing": TestingConfig, 

184} 

185 

186 

187__all__ = ["Config", "DevelopmentConfig", "ProductionConfig", "TestingConfig", 

188 "configure_app_logging", "PROJECT_ROOT"]