Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1600 allow providing default value in config #1604

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading