Configuration: settings and options¶
fresco.core.FrescoApp.options
is a dict-like
Options
object that can be used
for your application’s configuration.
Basic usage¶
In the simplest case, fresco.core.FrescoApp.options
can be used like a
regular dict:
from fresco import FrescoApp
app = FrescoApp()
app.options["SMTP_HOST"] = "smtp://localhost"
app.options.update({"DATABASE": "sqlite://"})
Settings are made available globally via context.app.options
:
from fresco import context
db = connect(context.app.options.DATABASE)
Using configuration files¶
Use load()
to load options from configuration
files:
app = FrescoApp()
# Load options from all matching files
app.options.load("conf.d/*")
# A fully loaded example
app.options.load(
["myapp.toml", "conf.d/*"],
# Search for files in this directory
dir="/path/to/config",
# Also load any of "myapp.{hostname}.*",
# "myapp.{FRESCO_PROFILE}.*", or "myapp.local.*", if found.
tags=["{hostname}", "{FRESCO_PROFILE}", "local"],
# Allow configuration files to add keys not present in the first file
strict=False,
# Use environment variables in preference to values loaded from files
use_environ=True
)
The dir
argument specifies the directory to be searched for configuration
files (defaults to the current directory).
The default behaviour is that the first loaded file must specify all available
configuration keys.
A subsequent file that introduces a new key will raise an exception.
Specify strict=False
to turn off this check.
If use_environ=True
is specified, any
environment variable with the same name as a loaded option key will be used in
preference to the value loaded from the file. Environment variables are subject
to the same parsing and interpolation rules as described in key-value
properties files’
Selecting configuration files based on environment¶
load()
can load files selectively based on
‘tags’ embedded in the file names (tags are embedded in the filename, separated
by dots, eg settings.tag1.tag2.conf
).
For example with these configuration files:
settings.conf # 1
settings.dev.conf # 2
settings.staging.conf # 3
settings.staging.local.conf # 4
settings.local.conf # 5
This is how to load subsets of the available files:
# Load all files
options.load("settings*")
# Load base config file plus those
# tagged 'dev' or 'local' (files #1, #2 and #5)
options.load("settings*", ["dev", "local"])
In addition to tags, filenames can contain priority numbers to allow a specific loading order to be achieved. Priority numbers go after the first dot, and may be separated from a tag with a hyphen. For example:
settings.100.conf
settings.200-local.conf
The string {hostname}
in a tag name will be interpolated with the current
hostname (as returned by socket.gethostname
), but with dots replaced by
underscores (eg myserver_example_org
). A common pattern is:
options.load(
"settings*",
[
os.environ.get("FRESCO_PROFILE", "prod"),
"host-{hostname}",
"local"
]
)
This will load:
a base config file (eg
settings.conf
);then any config files tagged with the profile specified in the
FRESCO_PROFILE
environment variable (egsettings.dev.conf
orsettings.prod.conf
)then any host-specific config files (eg
settings.host-myserver_example_org.conf
);finally, any local config files (eg
settings.local.conf
).
Configuration file loading order¶
Configuration files are sorted according to three criteria, from most to least important:
By priority number, if present, from low to high.
Then by any tags present in the filename, according to the order of tags provided to
load()
. Filenames containing tags specified later in this list are loaded later than those specified at the startFinally, in lexicographical order
To ensure that a configuration file is loaded last, and thus can override settings specified in earlier files, assign it a high priority number.
Processing configuration data¶
Fresco exposes functions to help load structured configuration data from strings. These may be useful when storing your configuration data as simple key-value pairs, or specifying options from environment variables.
list_from_str()
splits a string into a list on a
separator, stripping out intervening whitespace, for example:
from fresco.options import Options, list_from_str
options = Options(
ADMIN_EMAIL="me@mydomain.com, you@yourdomain.com",
)
# Convert to `['me@mydomain.com', 'you@yourdomain.com']`
options["ADMIN_EMAIL"] = list_from_str(options.ADMIN_EMAIL)
dict_from_options()
populates a dictionary from options
named with the same prefix.
from fresco.options import Options, dict_from_options
options = Options(
DATABASE_HOST="localhost",
DATABASE_USER="scott",
DATABASE_PASSWORD="tiger",
)
# Convert to {"HOST": "localhost", "USER": ...}
options["DATABASE"] = dict_from_options("DATABASE_", options)
When post-processing options in this way,
it’s a good idea to set trigger_onload=False
when calling load()
and then call trigger_onload()
once
processing is complete.
Configuration file formats¶
The following formats are supported by load()
:
key-value properties files¶
These are simple newline separated lists of key-value pairs. The format is designed to be easy to parse, and as such does not support features such as multi-line strings or escaping special characters. For more flexibility use one of the other formats.
Example:
# This is a comment
database = postgresql://myserver/mydb
# There are two special variables available:
config_path = $__FILE__
data_dir = $__DIR__/data
# Environment variables may also be interpolated
cache_dir = $HOME/.cache/myapp-cache
# As can configuration keys that have already been loaded
sessions_dir = $data_dir/sessions
# Boolean values: 'true' and 'false' are case-insensitive here
apples_are_green = true
horses_are_green = false
# Integers
x = 1
y = 2
# Values with decimal points are parsed as `decimal.Decimal` objects
z = 11.3
# Everything else is a string
hello = world
# Strings may be quoted if required to disambiguate
# or to preserve whitespace
apikey = "123456"
message = " HELLO $hello "
JSON (.json
)¶
JSON files are loaded using python’s builtin JSON module.
Toml (.toml
)¶
Loading from Toml files is only supported if the toml package is available.
Python files (.py
)¶
The file will be loaded and parsed as a Python module. Module-level variables are loaded into the options dict unless they start with an underscore.
The __all__
variable may be used to specify a list of keys to load.
Example:
# Use leading underscores to avoid creating
# unnecessary entries in the options dict
from os.path import join as _pathjoin
from os import environ as _env
DATABASE = f"sqlite:///{_env['HOME']}/my.db"
Loading settings from environment variables¶
If you also want to load your default settings from files and then update this
from environment variables,
use load()
:
app = FrescoApp()
app.options.load("myapp.conf", use_environ=True)
If you only want to load values from the environment, use
update()
:
app = FrescoApp()
settings_keys = ['DATABASE']
app.options.update({k: os.environ[k] for k in settings_keys})
Onload hooks¶
Register callbacks to be run once
load()
has completed by using
onload()
:
options = fresco.options.Options()
@options.onload
def do_something_with(options):
print("Howdy!")
options.load(".env*")
Overriding options for testing¶
override_options()
allows you to
temporarily override settings during testing:
from myproj import app
from fresco.options import override_options
with override_options(app.options, DATABASE="test_db"):
# Code inside this block will see the overridden value for
# app.options.DATABASE
...
Note that this globally modifies the options object, and is not thread safe. If you are using this in a multi-threaded environment other threads will see the overridden values, leading to unpredictable results.
API reference: fresco.options¶
- class fresco.options.Options(*a, **kw)[source]¶
Options dictionary. An instance of this is attached to each
fresco.core.FrescoApp
instance, as a central store for configuration options.- load(sources: str | Iterable[Path | str], tags: Sequence[str] = [], use_environ=False, strict=True, dir=None, trigger_onload=True) Options [source]¶
Find all files matching glob pattern
sources
and populates the options object from those with matching filenames containingtags
.- Parameters:
sources – one or more glob patterns separated by “;”, or a list of glob patterns
tags – a list of tags to look for in file names.
use_environ – if true, environment variables matching previously loaded keys will be loaded into the options object. This happens after all files have been processed.
strict – if true, the first file loaded is assumed to contain all available option keys. Any new key found in a later file will raise an error.
Files may be in python (
.py
), json (.json
), TOML (.toml
) format. Any other files will be interpreted as simple lists of`key=value
pairs.Filename format¶
The general format of filenames is:
<base>(.<priority number>-)(<tags>)(<suffix>)
Example filenames:
# Just <base> .env # <base>.<suffix> .settings.toml # <base>.<tags>.<suffix> .env.dev.local..py # <base>.<priority>-<tags>.<suffix> .env.100-dev.py
Priority number, if specified, is used to determine loading order. Lower numbers are loaded first. Priority numbers must be positive integers.
Tags are delimited with periods, for example
.env.production.py
. The filenamesetttings.dev.local.ini
would be considered to have the tags('dev', 'local')
Where filename contain multiple tags, all tags must match for the file to be loaded.
Tag names may contain the names of environment variable surrounded by braces, for example
{USER}
. These will be substituted for the environment variable’s value, with any dots or path separators replaced by underscores.The special variable
{hostname}
will be substituted for the current host’s name with dots replaced by underscores.Files with the suffix “.sample” are unconditionally excluded.
Loading order¶
Files are loaded in the following order:
On priority number, from low to high. If the priority number is not given, a priority of zero is assumed
Then in tag order, based on the ordering given in the
tags
parameterFor example, calling
options.load(..., tags=["dev", "local"])
would cause a file named “settings.dev” to be loaded before one named “settings.local”.Finally in lexicographical order.
Environment variables, if requested, are loaded last.
Example:
opts = Options() opts.load(".env*", ["dev", "host-{hostname}", "local"])
Would load options from files named
.env
,.env.json
,.env.dev.py
and.env.local.py
, in that order.
- onload(fn: Callable) Callable [source]¶
Register a function to be called once
load
has finished populating the options object.
- trigger_onload()[source]¶
Mark the options object as having loaded and call any registered onload callbacks
- update_from_dict(d, load_all=False)[source]¶
Update from the given list of key-value pairs.
If
load_all
is True, all key-value pairs will be loaded.Otherwise, if the special key ‘__all__’ is present, only those keys listed in __all__ will be loaded (same semantics as from … import *)
Otherwise only those NOT beginning with
_
will be loaded.