Coverage for app / config.py: 86%
90 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-06 04:49 +0000
« 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.
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
18from dotenv import load_dotenv
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"
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"
28# Load the appropriate .env file
29load_dotenv(str(dotenv_path))
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
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")
46 SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoid overhead of tracking
48 # Add any other app-wide default configurations here
49 DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t", "yes")
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")
58 # Generate a good salt for password hashing using: secrets.SystemRandom().getrandbits(128)
59 SECURITY_PASSWORD_SALT = os.environ.get("SECURITY_PASSWORD_SALT")
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')
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
73 PERMANENT_SESSION_LIFETIME = 3600 # 1 hour
74 SECURITY_TOKEN_MAX_AGE = 3600 # 1 hour for security tokens
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/"
81 SECURITY_EMAIL_SENDER = os.getenv("SECURITY_EMAIL_SENDER")
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
88 FLASK_ADMIN_SWATCH = "sandstone"
90 RATELIMIT_ENABLED = True
92 # Logging configuration - override in specific environments
93 LOGGING_LEVEL = logging.INFO # Default logging level
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
98 @classmethod
99 def configure_logging(cls):
100 """
101 Configures logging for the application.
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
109 # Configure SQLAlchemy specific logging
110 logging.getLogger("sqlalchemy.engine").setLevel(cls.LOGGING_LEVEL)
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
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
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}")
149 # pylint: disable=invalid-name
150 def __init__(self):
151 super().__init__()
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"
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.
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()
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}
187__all__ = ["Config", "DevelopmentConfig", "ProductionConfig", "TestingConfig",
188 "configure_app_logging", "PROJECT_ROOT"]