Skip to content

Commit

Permalink
1600 allow providing default value in config (#1604)
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardogsilva authored Mar 22, 2024
1 parent 421559a commit 2abb943
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 13 deletions.
20 changes: 20 additions & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,26 @@ Below is an example of how to integrate system environment variables in pygeoapi
host: ${MY_HOST}
port: ${MY_PORT}
Multiple environment variables are supported as follows:

.. code-block:: yaml
data: ${MY_HOST}:${MY_PORT}
It is also possible to define a default value for a variable in case it does not exist in
the environment using a syntax like: ``value: ${ENV_VAR:-the default}``

.. code-block:: yaml
server:
bind:
host: ${MY_HOST:-localhost}
port: ${MY_PORT:-5000}
metadata:
identification:
title:
en: This is pygeoapi host ${MY_HOST} and port ${MY_PORT:-5000}, nice to meet you!
Hierarchical collections
------------------------
Expand Down
42 changes: 29 additions & 13 deletions pygeoapi/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,23 +163,39 @@ def yaml_load(fh: IO) -> dict:
:returns: `dict` representation of YAML
"""

# support environment variables in config
# https://stackoverflow.com/a/55301129
path_matcher = re.compile(r'.*\$\{([^}^{]+)\}.*')

def path_constructor(loader, node):
env_var = path_matcher.match(node.value).group(1)
if env_var not in os.environ:
msg = f'Undefined environment variable {env_var} in config'
raise EnvironmentError(msg)
return get_typed_value(os.path.expandvars(node.value))
# # support environment variables in config
# # https://stackoverflow.com/a/55301129

env_matcher = re.compile(
r'.*?\$\{(?P<varname>\w+)(:-(?P<default>[^}]+))?\}')

def env_constructor(loader, node):
result = ""
current_index = 0
raw_value = node.value
for match_obj in env_matcher.finditer(raw_value):
groups = match_obj.groupdict()
varname_start = match_obj.span('varname')[0]
result += raw_value[current_index:(varname_start-2)]
if (var_value := os.getenv(groups['varname'])) is not None:
result += var_value
elif (default_value := groups.get('default')) is not None:
result += default_value
else:
raise EnvironmentError(
f'Could not find the {groups["varname"]!r} environment '
f'variable'
)
current_index = match_obj.end()
else:
result += raw_value[current_index:]
return get_typed_value(result)

class EnvVarLoader(yaml.SafeLoader):
pass

EnvVarLoader.add_implicit_resolver('!path', path_matcher, None)
EnvVarLoader.add_constructor('!path', path_constructor)

EnvVarLoader.add_implicit_resolver('!env', env_matcher, None)
EnvVarLoader.add_constructor('!env', env_constructor)
return yaml.load(fh, Loader=EnvVarLoader)


Expand Down
36 changes: 36 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from decimal import Decimal
from contextlib import nullcontext as does_not_raise
from copy import deepcopy
from io import StringIO
from unittest import mock

import pytest
from pyproj.exceptions import CRSError
Expand Down Expand Up @@ -77,6 +79,40 @@ def test_yaml_load(config):
util.yaml_load(fh)


@pytest.mark.parametrize('env,input_config,expected', [
pytest.param({}, 'foo: something', {'foo': 'something'}, id='no-env-expansion'), # noqa E501
pytest.param({'FOO': 'this'}, 'foo: ${FOO}', {'foo': 'this'}), # noqa E501
pytest.param({'FOO': 'this'}, 'foo: the value is ${FOO}', {'foo': 'the value is this'}, id='no-need-for-yaml-tag'), # noqa E501
pytest.param({}, 'foo: ${FOO:-some default}', {'foo': 'some default'}), # noqa E501
pytest.param({'FOO': 'this', 'BAR': 'that'}, 'composite: ${FOO}:${BAR}', {'composite': 'this:that'}), # noqa E501
pytest.param({}, 'composite: ${FOO:-default-foo}:${BAR:-default-bar}', {'composite': 'default-foo:default-bar'}), # noqa E501
pytest.param(
{
'HOST': 'fake-host',
'USER': 'fake',
'PASSWORD': 'fake-pass',
'DB': 'fake-db'
},
'connection: postgres://${USER}:${PASSWORD}@${HOST}:${PORT:-5432}/${DB}', # noqa E501
{
'connection': 'postgres://fake:fake-pass@fake-host:5432/fake-db'
},
id='multiple-no-need-yaml-tag'
),
])
def test_yaml_load_with_env_variables(
env: dict[str, str], input_config: str, expected):

def mock_get_env(env_var_name):
result = env.get(env_var_name)
return result

with mock.patch('pygeoapi.util.os') as mock_os:
mock_os.getenv.side_effect = mock_get_env
loaded_config = util.yaml_load(StringIO(input_config))
assert loaded_config == expected


def test_str2bool():
assert not util.str2bool(False)
assert not util.str2bool('0')
Expand Down

0 comments on commit 2abb943

Please sign in to comment.