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

Feature request: Cascaded settings files #142

Closed
williwacker opened this issue Jul 31, 2022 · 8 comments
Closed

Feature request: Cascaded settings files #142

williwacker opened this issue Jul 31, 2022 · 8 comments

Comments

@williwacker
Copy link

williwacker commented Jul 31, 2022

Hi,
I am running Django with multiple tenants. For each tenant I am using a different settings.ini file. But each of this files has common parameters like e.g. DB credentials.
In order to only maintain these kind of common parameters only once I would like to have one common-settings.ini file beside the tenant specific settings.ini files. The settings.py is stored in GIT and therefore cannot save credentials. Would it be possible to read more than one settings file?
Thanks

@lucasrcezimbra
Copy link
Member

I don't think that multiple files make sense for python-decouple. One of the main ideas of the lib is to avoid multiple settings files by reading variables from the environment.

If you really need this multiple files behavior, you can try implementing an extension as described in #115.

@RamonGiovane
Copy link

RamonGiovane commented Aug 5, 2022

What if I have 3 environments? A development environment, a testing enviroment and a production environment.
They have different variables, like different database connection strings, different API keys.
Should I place all those variables in the same file, like this?

TEST_DB = 'postgres//user:password@localhost/test
DEV_DB = 'postgres/user:passoword@localhost/dev'
PROD_DB = 'postgres/user:[email protected]/database'

I wish I could have:

DB = config('DB')

And config loads the env from a file accordingly the actual environment. Is it possible?

@lucasrcezimbra
Copy link
Member

What if I have 3 environments? A development environment, a testing enviroment and a production environment. They have different variables, like different database connection strings, different API keys. Should I place all those variables in the same file, like this?

TEST_DB = 'postgres//user:password@localhost/test
DEV_DB = 'postgres/user:passoword@localhost/dev'
PROD_DB = 'postgres/user:[email protected]/database'

I wish I could have:

DB = config('DB')

And config loads the env from a file accordingly the actual environment. Is it possible?

In the development environment you will have:

DB = 'postgres//user:password@localhost/dev'

In the testing environment you will have:

DB = 'postgres//user:password@localhost/test'

In the prod environment you will have:

DB = 'postgres/user:[email protected]/database'

@b0o
Copy link
Contributor

b0o commented Feb 12, 2023

Here's what I'm using to cascade multiple repositories:

from decouple import Config, RepositoryEnv, RepositoryEmpty


class RepositoryComp(RepositoryEmpty):
    def __init__(self, *repositories):
        self.repositories = repositories

    def __getitem__(self, key):
        for repository in self.repositories:
            if repository.__contains__(key):
                return repository[key]
        raise KeyError(key)

    def __contains__(self, key):
        for repository in self.repositories:
            if repository.__contains__(key):
                return True
        return False


config = Config(RepositoryComp(RepositoryEnv('.private.env'), RepositoryEnv('.env')))

@henriquebastos
Copy link
Collaborator

Hey @b0o. This is cool. I wonder if using a ChainMap wound be enough. I didn't tested, but would be something like:

from collections import ChainMap
from decouple import Config, RepositoryEnv

config = Config(ChainMap(RepositoryEnv(".private.env"), RepositoryEnv(".env")))

@b0o
Copy link
Contributor

b0o commented Feb 16, 2023

@henriquebastos That does work with RepositoryEnv envs but not RepositoryIni envs. For example

private.ini:

[settings]
FOO=Hello

config.ini:

[settings]
BAR=Goodbye
In [1]: from collections import ChainMap
   ...: from decouple import Config, RepositoryIni
   ...:
   ...: config = Config(ChainMap(RepositoryIni("private.ini"), RepositoryIni("config.ini")))

In [2]: config.get("FOO")
Out[2]: 'Hello'

In [3]: config.get("BAR")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/.asdf/installs/python/3.10.2/lib/python3.10/configparser.py:790, in RawConfigParser.get(self, section, option, raw, vars, fallback)
    789 try:
--> 790     value = d[option]
    791 except KeyError:

File ~/.asdf/installs/python/3.10.2/lib/python3.10/collections/__init__.py:982, in ChainMap.__getitem__(self, key)
    981         pass
--> 982 return self.__missing__(key)

File ~/.asdf/installs/python/3.10.2/lib/python3.10/collections/__init__.py:974, in ChainMap.__missing__(self, key)
    973 def __missing__(self, key):
--> 974     raise KeyError(key)

KeyError: 'bar'

During handling of the above exception, another exception occurred:

NoOptionError                             Traceback (most recent call last)
Cell In[3], line 1
----> 1 config.get("BAR")

File ~/.local/share/virtualenvs/venv-BR9r7xws/lib/python3.10/site-packages/decouple.py:89, in Config.get(self, option, default, cast)
     87     value = os.environ[option]
     88 elif option in self.repository:
---> 89     value = self.repository[option]
     90 else:
     91     if isinstance(default, Undefined):

File ~/.asdf/installs/python/3.10.2/lib/python3.10/collections/__init__.py:979, in ChainMap.__getitem__(self, key)
    977 for mapping in self.maps:
    978     try:
--> 979         return mapping[key]             # can't use 'key in mapping' with defaultdict
    980     except KeyError:
    981         pass

File ~/.local/share/virtualenvs/venv-BR9r7xws/lib/python3.10/site-packages/decouple.py:137, in RepositoryIni.__getitem__(self, key)
    136 def __getitem__(self, key):
--> 137     return self.parser.get(self.SECTION, key)

File ~/.asdf/installs/python/3.10.2/lib/python3.10/configparser.py:793, in RawConfigParser.get(self, section, option, raw, vars, fallback)
    791 except KeyError:
    792     if fallback is _UNSET:
--> 793         raise NoOptionError(option, section)
    794     else:
    795         return fallback

NoOptionError: No option 'bar' in section: 'settings'

If RepositoryIni is updated to except a NoOptionError and re-raise a KeyError, it works:

In [35]: from decouple import RepositoryEmpty, read_config, DEFAULT_ENCODING
    ...: from configparser import ConfigParser, NoOptionError
    ...: class RepositoryIni(RepositoryEmpty):
    ...:     """
    ...:     Retrieves option keys from .ini files.
    ...:     """
    ...:     SECTION = 'settings'
    ...:
    ...:     def __init__(self, source, encoding=DEFAULT_ENCODING):
    ...:         self.parser = ConfigParser()
    ...:         with open(source, encoding=encoding) as file_:
    ...:             read_config(self.parser, file_)
    ...:
    ...:     def __contains__(self, key):
    ...:         return (key in os.environ or
    ...:                 self.parser.has_option(self.SECTION, key))
    ...:
    ...:     def __getitem__(self, key):
    ...:         try:
    ...:             return self.parser.get(self.SECTION, key)
    ...:         except NoOptionError:
    ...:             raise KeyError(key)
    ...:

In [36]: config = Config(ChainMap(RepositoryIni("private.ini"), RepositoryIni("config.ini")))

In [37]: config.get("FOO")
Out[37]: 'Hello'

In [38]: config.get("BAR")
Out[38]: 'Goodbye'

b0o added a commit to b0o/python-decouple that referenced this issue Feb 16, 2023
Enables RepositoryIni to be used in a collections.ChainMap
to allow cascading multiple repositories.

See HBNetwork#142
@henriquebastos
Copy link
Collaborator

Great work, @b0o! Thank you.

This was referenced Mar 2, 2023
@danrossi
Copy link

danrossi commented Mar 7, 2023

After multiple attempts this works. Putting the overriding configs before the default configs.

config = Config(ChainMap(RepositoryEnv(".custom.env"), RepositoryEnv(".env")))
print(config('AGENT_PUBLIC_KEY'))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants