-
-
Save ryan-blunden/fc9fbaf2da65dd2200fb997bfb0aa365 to your computer and use it in GitHub Desktop.
Python Application Config and Secrets Class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
API_KEY="357A70FF-BFAA-4C6A-8289-9831DDFB2D3D" | |
HOSTNAME="0.0.0.0" | |
PORT="8080" | |
# Optional | |
# DEBUG="True" | |
# ENV="development" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from config import Config | |
# Individual key access | |
print('ENV: {}'.format(Config.ENV)) | |
print('DEBUG: {}'.format(Config.DEBUG)) | |
print('API_KEY: {}'.format(Config.API_KEY)) | |
print('HOSTNAME: {}'.format(Config.HOSTNAME)) | |
print('PORT: {}'.format(Config.PORT)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
from typing import get_type_hints, Union | |
from dotenv import load_dotenv | |
load_dotenv() | |
class AppConfigError(Exception): | |
pass | |
def _parse_bool(val: Union[str, bool]) -> bool: # pylint: disable=E1136 | |
return val if type(val) == bool else val.lower() in ['true', 'yes', '1'] | |
# AppConfig class with required fields, default values, type checking, and typecasting for int and bool values | |
class AppConfig: | |
DEBUG: bool = False | |
ENV: str = 'production' | |
API_KEY: str | |
HOSTNAME: str | |
PORT: int | |
""" | |
Map environment variables to class fields according to these rules: | |
- Field won't be parsed unless it has a type annotation | |
- Field will be skipped if not in all caps | |
- Class field and environment variable name are the same | |
""" | |
def __init__(self, env): | |
for field in self.__annotations__: | |
if not field.isupper(): | |
continue | |
# Raise AppConfigError if required field not supplied | |
default_value = getattr(self, field, None) | |
if default_value is None and env.get(field) is None: | |
raise AppConfigError('The {} field is required'.format(field)) | |
# Cast env var value to expected type and raise AppConfigError on failure | |
try: | |
var_type = get_type_hints(AppConfig)[field] | |
if var_type == bool: | |
value = _parse_bool(env.get(field, default_value)) | |
else: | |
value = var_type(env.get(field, default_value)) | |
self.__setattr__(field, value) | |
except ValueError: | |
raise AppConfigError('Unable to cast value of "{}" to type "{}" for "{}" field'.format( | |
env[field], | |
var_type, | |
field | |
) | |
) | |
def __repr__(self): | |
return str(self.__dict__) | |
# Expose Config object for app to import | |
Config = AppConfig(os.environ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
python-dotenv |
Thanks @alexmojaki. I've updated the code as per your suggestion and also provided a link to Pydantic in the article this code belongs to - https://doppler.com/blog/environment-variables-in-python
It's better to use the is
operator for this line:
if default_value == None and env.get(field) == None:
if default_value is None and env.get(field) is None:
Thanks @GreatBahram, code has been updated.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This kind of thing is done in https://pydantic-docs.helpmanual.io/usage/settings/. I've also written a small wrapper around that: https://github.com/alexmojaki/dryenv
This code is a bit weird:
get_type_hints(AppConfig)[field]
is already the actual class such asstr
. Why get__name__
and then get it frombuiltins
again? Do you want to only allow builtins and not other types? Also note that this will not work withbool
in an intuitive way.