flask-env icon indicating copy to clipboard operation
flask-env copied to clipboard

A way to specify required environment variables

Open okke-formsma opened this issue 8 years ago • 5 comments

It would be nice to have a way to specify which environment variables are required to be set, so the flask app will quit early when there are environment variables missing.

okke-formsma avatar Oct 24 '17 09:10 okke-formsma

@okke-formsma thanks for the suggestion!

Do you have an idea on how this will work? Is there a special way you define certain values that are required?

I think this is a good idea, I am trying to think about how it plays with #2, since that says "if you didn't predefine a setting, then it won't get loaded".

brettlangdon avatar Oct 24 '17 12:10 brettlangdon

@brettlangdon, I was thinking along the lines of setting the required fields to a specific value, something like

from flask_env import RequiredEnvironmentValue
class StagingConfig(metaclass=MetaFlaskEnv):
    ENV_PREFIX = 'FLASK_'
    SECRET_KEY = RequiredEnvironmentValue
    ELASTICSEARCH_HOST = RequiredEnvironmentValue
    SQLALCHEMY_DATABASE_URI = RequiredEnvironmentValue

okke-formsma avatar Oct 24 '17 12:10 okke-formsma

Alright, I just had a idea... feel free to tell me I am crazy or complicating things. I was trying to think about other use cases, like mapping env variables to different names, or explicitly casting values to a specific type, or making certain ones required, etc etc. So I came up with the following idea/solution.

from flask_env import MetaFlaskEnv, Loader

class Settings(metaclass=MetaFlaskEnv):
    # This value is required, raise exception if it is not provided
    SECRET_KEY = Loader.required()
    
    # Load this value from the `DB_URI` env variable, and it is required to be set
    SQLALCHEMY_DATABASE_URI = Loader.name('DB_URI').required()

    # Load this value from `PORT`, default to `8000`, and explicitly cast the value to an `int`
    PORT = Loader.default(8000).cast(int)

    # Can still assign defaults like normal
    DEBUG = True
    Elasticsearch_HOST = 'localhost'    

brettlangdon avatar Oct 24 '17 16:10 brettlangdon

That's nice. I'm not a very big fan of the chained commands. Maybe just use optional parameters instead? Also, I think variables should be implicitly required, unless a default value is provided.

from flask_env import MetaFlaskEnv, EnvLoader

class Settings(metaclass=MetaFlaskEnv):
	# This value is required, raise exception if it is not provided
	SECRET_KEY = EnvLoader()
	
	# Load this value from the `DB_URI` env variable, and it is required to be set
	SQLALCHEMY_DATABASE_URI = EnvLoader(name='DB_URI')

	# Load this value from `PORT`, default to `8000`, and explicitly cast the value to an `int`
	PORT = EnvLoader(default=8000, cast=int)

	# Can still assign defaults like normal
	DEBUG = True
	Elasticsearch_HOST = 'localhost'  

okke-formsma avatar Oct 25 '17 09:10 okke-formsma

I gave this a stab for myself, feel free to use it in any way.

This is based on the descriptor pattern (https://docs.python.org/2/howto/descriptor.html)


class EnvVar():
    def __init__(self, name=None, default=None, cast=None):
        """ Load environment name `name`. Use default value `default`. Raises an ValueError if no environment variable
        is found and no default is set.  Will cast automatically to the type `cast` or to the type of `default` if not 
        set.
        
        Example:

            DevelopmentConfig():
                DEBUG = EnvVar('FLASK_DEBUG', default=False)
                PROFILE_RATE = EnvVar(cast=float)
                SQLALCHEMY_DATABASE_URI = EnvVar('DB')
        """
        self.name = name
        self.default = default
        if cast is None:
            self.cast = type(default)
        else:
            self.cast = cast
        self.value = None

    def __get__(self, obj, objtype=None):
        value = os.getenv(self.name)
        if value is None:
            if self.default is None:
                raise ValueError("Environment variable `{}` was expected to be set.".format(self.name))
            else:
                return self.default
        
        if self.cast is None:
            return value

        return self.cast(value)

    def __set__(self, obj, value):
        raise ValueError("This variable can not be set but will be initiated from environment values.")

okke-formsma avatar Nov 13 '17 10:11 okke-formsma