Skip to content

Instantly share code, notes, and snippets.

@ryan-blunden

ryan-blunden/.env

Last active Feb 1, 2021
Embed
What would you like to do?
Python Application Config and Secrets Class
API_KEY="357A70FF-BFAA-4C6A-8289-9831DDFB2D3D"
HOSTNAME="0.0.0.0"
PORT="8080"
# Optional
# DEBUG="True"
# ENV="development"
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))
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)
@alexmojaki

This comment has been minimized.

Copy link

@alexmojaki alexmojaki commented Jan 27, 2021

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:

                var_type = get_type_hints(AppConfig)[field].__name__
                value = getattr(builtins, var_type)(env.get(field, default_value))

get_type_hints(AppConfig)[field] is already the actual class such as str. Why get __name__ and then get it from builtins again? Do you want to only allow builtins and not other types? Also note that this will not work with bool in an intuitive way.

@ryan-blunden

This comment has been minimized.

Copy link
Owner Author

@ryan-blunden ryan-blunden commented Jan 27, 2021

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

@GreatBahram

This comment has been minimized.

Copy link

@GreatBahram GreatBahram commented Jan 30, 2021

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:
@ryan-blunden

This comment has been minimized.

Copy link
Owner Author

@ryan-blunden ryan-blunden commented Feb 1, 2021

Thanks @GreatBahram, code has been updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment