diff --git a/CHANGES.rst b/CHANGES.rst index a2d2af7e..d815f950 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,13 @@ +Added +^^^^^ +- `env_prefix` create_app variable can select the environment var prefix. + [0.0.52] - 2024-04-22 --------------------- Added ^^^^^ -- `ENV_FILE` environment variable can customize/disable the .env file +- `env_file` create_app variable can customize/disable the .env file Changed ^^^^^^^ diff --git a/canaille/__init__.py b/canaille/__init__.py index cf8f3088..84103f61 100644 --- a/canaille/__init__.py +++ b/canaille/__init__.py @@ -124,7 +124,9 @@ def setup_flask_converters(app): app.url_map.converters[model_name.lower()] = model_converter(model_class) -def create_app(config=None, validate=True, backend=None): +def create_app( + config=None, validate=True, backend=None, env_file=".env", env_prefix="" +): from .app.configuration import setup_config from .app.i18n import setup_i18n from .app.themes import setup_themer @@ -132,7 +134,13 @@ def create_app(config=None, validate=True, backend=None): app = Flask(__name__) with app.app_context(): - if not setup_config(app, config, validate): # pragma: no cover + if not setup_config( + app=app, + config=config, + test_config=validate, + env_file=env_file, + env_prefix=env_prefix, + ): # pragma: no cover sys.exit(1) sentry_sdk = setup_sentry(app) diff --git a/canaille/app/configuration.py b/canaille/app/configuration.py index 16553fb5..a42c834c 100644 --- a/canaille/app/configuration.py +++ b/canaille/app/configuration.py @@ -13,7 +13,6 @@ from pydantic_settings import SettingsConfigDict from canaille.core.configuration import CoreSettings ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DEFAULT_ENV_FILE = ".env" class RootSettings(BaseSettings): @@ -60,10 +59,10 @@ class RootSettings(BaseSettings): """ -def settings_factory(config): - """Overly complicated function that pushes the backend specific - configuration into CoreSettings, in the purpose break dependency against - backends libraries like python-ldap or sqlalchemy.""" +def settings_factory(config, env_file=".env", env_prefix=""): + """Pushes the backend specific configuration into CoreSettings, in the + purpose break dependency against backends libraries like python-ldap or + sqlalchemy.""" attributes = {"CANAILLE": (CoreSettings, CoreSettings())} if "CANAILLE_SQL" in config or any( @@ -93,11 +92,11 @@ def settings_factory(config): **attributes, ) - env_file = os.getenv("ENV_FILE", config.get("ENV_FILE", DEFAULT_ENV_FILE)) return Settings( **config, _secrets_dir=os.environ.get("SECRETS_DIR"), _env_file=env_file, + _env_prefix=env_prefix, ) @@ -121,7 +120,7 @@ def toml_content(file_path): raise Exception("toml library not installed. Cannot load configuration.") -def setup_config(app, config=None, test_config=True): +def setup_config(app, config=None, test_config=True, env_file=".env", env_prefix=""): from canaille.oidc.installation import install app.config.from_mapping( @@ -135,7 +134,9 @@ def setup_config(app, config=None, test_config=True): config = toml_content(os.environ.get("CONFIG")) try: - config_obj = settings_factory(config or {}) + config_obj = settings_factory( + config or {}, env_file=env_file, env_prefix=env_prefix + ) except ValidationError as exc: # pragma: no cover app.logger.critical(str(exc)) return False diff --git a/tests/app/test_configuration.py b/tests/app/test_configuration.py index 0b66ba6f..36c43b9b 100644 --- a/tests/app/test_configuration.py +++ b/tests/app/test_configuration.py @@ -41,6 +41,7 @@ def test_configuration_nestedsecrets_directory(tmp_path, backend, configuration) def test_configuration_from_environment_vars(): + """Canaille should read configuration from environment vars.""" os.environ["SECRET_KEY"] = "very-very-secret" os.environ["CANAILLE__SMTP__FROM_ADDR"] = "user@mydomain.tld" os.environ["CANAILLE_SQL__DATABASE_URI"] = "sqlite:///anything.db" @@ -60,6 +61,22 @@ def test_configuration_from_environment_vars(): del os.environ["CANAILLE_SQL__DATABASE_URI"] +def test_disable_env_var_loading(tmp_path, configuration): + """Canaille should not read configuration from environment vars when + env_prefix is False.""" + del configuration["SERVER_NAME"] + os.environ["SERVER_NAME"] = "example.com" + os.environ["FOOBAR_SERVER_NAME"] = "foobar.example.com" + + app = create_app(configuration, env_prefix="") + assert app.config["SERVER_NAME"] == "example.com" + + app = create_app(configuration, env_prefix="FOOBAR_") + assert app.config["SERVER_NAME"] == "foobar.example.com" + + del os.environ["SERVER_NAME"] + + def test_dotenv_file(tmp_path, configuration): """Canaille should read configuration from .env files.""" oldcwd = os.getcwd() @@ -81,8 +98,7 @@ def test_custom_dotenv_file(tmp_path, configuration): with open(dotenv, "w") as fd: fd.write("FOOBAR=other-custom-value") - configuration["ENV_FILE"] = dotenv - app = create_app(configuration) + app = create_app(configuration, env_file=dotenv) assert app.config["FOOBAR"] == "other-custom-value" @@ -95,8 +111,7 @@ def test_disable_dotenv_file(tmp_path, configuration): with open(dotenv, "w") as fd: fd.write("FOOBAR=custom-value") - configuration["ENV_FILE"] = None - app = create_app(configuration) + app = create_app(configuration, env_file=None) assert "FOOBAR" not in app.config os.chdir(oldcwd)