Source code for clout._loaders.env

import os
import typing as t

import attr
import desert
import glom
import inflection
import marshmallow

from .. import _util


@attr.dataclass
class Env:
    app_name: str = None
    metadata_key: str = "env"
    inherits: t.List[str] = frozenset({"app_name"})
    env: t.Dict[str, t.Any] = attr.ib(factory=dict)
    prefix: str = ""

    def make_path_to_field(
        self, schema: marshmallow.Schema, path=()
    ) -> t.Dict[str, marshmallow.fields.Field]:

        d = {}
        for field in schema.fields.values():

            if isinstance(field, marshmallow.fields.Nested):
                recursed = self.make_path_to_field(
                    field.schema, path=path + (field.name,)
                )
                d.update(recursed)

            elif isinstance(field, marshmallow.fields.Field):
                d[path + (field.name,)] = field
            else:
                raise TypeError(field)

        return d

    def make_envvar_name(self, path: t.Tuple[str]) -> str:
        prefix = self.prefix if self.prefix else self.app_name
        return inflection.underscore("_".join((prefix,) + path)).upper()

    def prep(self, typ, metadata=None, default=None, env=None, name=None):
        # TODO make sure this handles lists correctly.
        # If a field is a list, this should return a list, not a single member.
        metadata = metadata or {}

        schema = desert.schema_class(typ)()

        path_to_field = self.make_path_to_field(schema, path=())

        d = {}
        for path, field in path_to_field.items():
            name = self.make_envvar_name(path)

            value = os.environ.get(name)

            if value is not None:
                d[path] = field.deserialize(value)

        nested = make_nested(d)

        try:
            return nested
        except KeyError:
            return {}

    def set(self, **kw):
        return attr.evolve(self, **kw)


def make_nested(path_to_value: t.Dict[t.Tuple, t.Any]) -> dict:
    d = {}
    for path, value in sorted(
        path_to_value.items(), key=lambda path_value: len(path_value[0])
    ):
        func = dict
        glom.assign(d, ".".join(path), value, missing=func)
    return d


[docs]def load_env(type: t.Type, prefix: str = ""): """Load environment variables for a class into a dict. For example, define a class .. code-block:: python import attr @attr.dataclass class Database: host: str port: int Load the environment variables into a dict, setting the prefix for our app. .. code-block:: python d = clout.load_env(Database, prefix='MYAPP') assert d == {'host': 'example.com', 'port': 1234} Run the app with environment variables set. .. code-block:: bash export MYAPP_HOST=example.com export MYAPP_PORT=1234 """ return Env(prefix=prefix).prep(type)