e2e.env 0.1.0
Easily model and convert environment variables you care about.
License: Apache 2.0

# e2e.env

_Class-based modeling of environment variables and their type conversions._

Especially suited to test automation where environment variables are used in
abundance, or even in applications that could benefit from these mappings.

## Requirements

For ease of implementation, Python 3.6 is required in order to use
[PEP-526](https://www.python.org/dev/peps/pep-0526/) variable
annotations.

## Overview

Environment variable access is common, and usually done via a module-based
approach.

```python
SERVICE_HOST = os.getenv('COMPANY_APP_HOST')
SERVICE_PORT = int(os.getenv('COMPANY_APP_PORT'))
```

This works just fine, but sometimes they need to be refreshed...

```python
# Whoops, forgot the int conversion!
SERVICE_PORT = os.getenv('COMPANY_APP_PORT')
```

... which isn't very [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

Additionally, you may sometimes need to keep track of the name _and_ the value from the environment,

```python
# This is getting verbose quickly
ENVNAME_SERVICE_HOST = 'COMPANY_APP_HOST'
SERVICE_HOST = os.getenv(ENVNAME_SERVICE_HOST)
ENVNAME_SERVICE_PORT = 'COMPANY_APP_PORT'
SERVICE_PORT = int(os.getenv(ENVNAME_SERVICE_PORT))
```

In one swoop, we can map the environment variables as well as their
conversions to native types, and fetch either the value or the name:

```python
class ServiceVars(e2e.env.EnvMapper):
    host: str = 'COMPANY_APP_HOST'
    port: int = 'COMPANY_APP_PORT'

# Get the port via instance
print(ServiceVars().port)   # 8080
type(ServiceVars().port)    # <class 'int'>

# Get the name of the port environment variable via class
print(ServiceVars.port)     # COMPANY_APP_PORT
type(ServiceVars.port)      # <class 'str'>
```

## Using your own "converters"

All `EnvMapper` does is read the type annotation and constructs the returned
value by passing the environment value to it.

That is, when modeling,
```python
    mapped_name: annotated_type = 'ENV_VAR_NAME'
```
... on access via the `EnvMapper` instance, becomes ...
```python
    annotated_type(os.getenv('ENV_VAR_NAME'))
```

In the above examples for example, we had `port: int = 'COMPANY_APP_PORT'`.
This essentially gets shuffled into `int(os.getenv('COMPANY_APP_PORT'))`. So
any callable that can take a single `str` in its constructor and return the
appropriate type will work.

## Production use

The code is incredibly simple, and will adhere to these contracts:

- Variables that do not exist will cause a
  `e2e.env.exceptions.NoSuchVariableError` to be raised.\*
- Access of an unmapped environment variable will raise an `AttributeError`, as
  would be reasonably expected.
- Access of a mapping without an annotation will raise a `TypeError` with the
  mapping name and model class.

\* Open for discussion. Returning `None` could work. Passing `None` to the type
converter usually won't produce consistent behaviour across types, and so can't
be determined as a special case (e.g. `str(None)` gives `"None"`, `int(None)`
raises a `TypeError`). See
[Issue #1](https://github.com/nickroeker/e2e.env/issues/1) for more info.

## Future work

- Support `raise_on_dne` or something similar to change what happens when an
  environment variable is not found. Please add a thumbs-up for
  [Issue #1](https://github.com/nickroeker/e2e.env/issues/1) if you'd like to
  see this feature.
    ```python
    class ServiceVars(e2e.env.EnvMapper, raise_on_dne=False): ...
    ```
    ```python
    class ServiceVars(e2e.env.EnvMapper, dne=lambda: None): ...
    ```
- Support for combining mappings into one larger mapping, for organizational
  purposes. Please add a thumbs-up for
  [Issue #2](https://github.com/nickroeker/e2e.env/issues/2) if you'd like to
  see this feature.