Skip to content

Commit

Permalink
feat: load configuration from local 'canaille.toml' files
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Jan 29, 2025
1 parent c1ffc34 commit 2a50a84
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Added
- Add screenshots in the documentation. :issue:`210`
- Implement a ``canaille run`` command that runs a production server with Hypercorn. :pr:`219`
- Implement a ``canaille export-config`` command that create a commented config file. :pr:`223`
- Load configuration from local ``canaille.toml`` files. :pr:`225`

Changed
^^^^^^^
Expand Down
3 changes: 2 additions & 1 deletion canaille/app/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,14 @@ def export_config(path: Path | None):
or the :envvar:`CONFIG` environment variable if set, or a ``config.toml``
file in the current directory.
"""
from canaille.app.configuration import DEFAULT_CONFIG_FILE
from canaille.app.configuration import export_config
from canaille.app.configuration import settings_factory

config_obj = settings_factory(
current_app.config, all_options=True, init_with_examples=True
)
config_file = path or os.getenv("CONFIG", "config.toml")
config_file = path or os.getenv("CONFIG", DEFAULT_CONFIG_FILE)
export_config(config_obj, config_file)


Expand Down
13 changes: 10 additions & 3 deletions canaille/app/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
HAS_TOMLKIT = False
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

DEFAULT_CONFIG_FILE = "canaille.toml"


class BaseModel(PydanticBaseModel):
model_config = SettingsConfigDict(
Expand Down Expand Up @@ -174,9 +176,14 @@ def setup_config(app, config=None, test_config=True, env_file=None, env_prefix="
"SESSION_COOKIE_NAME": "canaille",
}
)
if HAS_TOMLKIT and not config and "CONFIG" in os.environ:
with open(os.environ.get("CONFIG")) as fd:
config = tomlkit.load(fd)
if HAS_TOMLKIT and not config:
if "CONFIG" in os.environ:
with open(os.environ.get("CONFIG")) as fd:
config = tomlkit.load(fd)

elif os.path.exists(DEFAULT_CONFIG_FILE):
with open(DEFAULT_CONFIG_FILE) as fd:
config = tomlkit.load(fd)

env_file = env_file or os.getenv("ENV_FILE")
try:
Expand Down
3 changes: 2 additions & 1 deletion doc/references/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ Configuration methods priority
If a same configuration option is defined by different ways, here is how Canaille will choose which one to use:

- environment vars have priority over the environment file and the configuration file;
- environment file will have priority over the configuration file.
- environment file will have priority over the configuration file;
- if no configuration method is used, Canaille will look for a ``canaille.toml`` configuration file in the current working directory.

Parameters
==========
Expand Down
43 changes: 43 additions & 0 deletions tests/app/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pathlib

import pytest
import tomlkit
from flask_webtest import TestApp

from canaille import create_app
Expand Down Expand Up @@ -44,6 +45,46 @@ def test_configuration_nestedsecrets_directory(tmp_path, backend, configuration)
del os.environ["SECRETS_DIR"]


def test_no_configuration(configuration, tmp_path):
"""Test that no configuration still makes valid Canaille application."""
os.environ["DEBUG"] = "1"

app = create_app()
assert app.config["CANAILLE"]["NAME"] == "Canaille"

del os.environ["DEBUG"]


def test_environment_configuration(configuration, tmp_path):
"""Test loading the configuration from a toml file passed by the CONFIG environment var."""
config_path = os.path.join(tmp_path, "config.toml")
with open(config_path, "w") as fd:
tomlkit.dump(configuration, fd)

os.environ["CONFIG"] = config_path
app = create_app()
assert app.config["CANAILLE"]["SMTP"]["FROM_ADDR"] == "admin@mydomain.test"

del os.environ["CONFIG"]
os.remove(config_path)


def test_local_configuration(configuration, tmp_path):
"""Test loading the configuration from a local config.toml file."""
cwd = os.getcwd()
os.chdir(tmp_path)

config_path = os.path.join(tmp_path, "canaille.toml")
with open(config_path, "w") as fd:
tomlkit.dump(configuration, fd)

app = create_app()
assert app.config["CANAILLE"]["SMTP"]["FROM_ADDR"] == "admin@mydomain.test"

os.chdir(cwd)
os.remove(config_path)


def test_configuration_from_environment_vars(tmp_path):
"""Canaille should read configuration from environment vars."""
os.environ["SECRET_KEY"] = "very-very-secret"
Expand Down Expand Up @@ -325,6 +366,7 @@ def test_smpp_connection_remote_smpp_no_credentials(
def test_no_secret_key(configuration, caplog):
del configuration["SECRET_KEY"]

os.environ["DEBUG"] = "1"
from canaille.app.server import app

assert (
Expand All @@ -338,6 +380,7 @@ def test_no_secret_key(configuration, caplog):
res.mustcontain(
"Your Canaille instance is not fully configured and not ready for production."
)
del os.environ["DEBUG"]


def test_sanitize_rst():
Expand Down
18 changes: 0 additions & 18 deletions tests/app/test_flaskutils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import os

import tomlkit

from canaille import create_app
from canaille.app.flask import set_parameter_in_url_query


Expand All @@ -21,16 +16,3 @@ def test_set_parameter_in_url_query():
set_parameter_in_url_query("https://auth.mydomain.test?foo=baz", hello="world")
== "https://auth.mydomain.test?foo=baz&hello=world"
)


def test_environment_configuration(configuration, tmp_path):
config_path = os.path.join(tmp_path, "config.toml")
with open(config_path, "w") as fd:
tomlkit.dump(configuration, fd)

os.environ["CONFIG"] = config_path
app = create_app()
assert app.config["CANAILLE"]["SMTP"]["FROM_ADDR"] == "admin@mydomain.test"

del os.environ["CONFIG"]
os.remove(config_path)

0 comments on commit 2a50a84

Please sign in to comment.