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)