envarguments 1.0.1
Utility to specify which environment variable shall be used as arguments for a program.
Requires Python: >=3.7

# envargs
[![codecov](https://codecov.io/gh/maansthoernvik/envargs/branch/master/graph/badge.svg?token=HXYFRI3THX)](https://codecov.io/gh/maansthoernvik/envargs)
[![PyPI version](https://badge.fury.io/py/envarguments.svg)](https://badge.fury.io/py/envarguments)

`envargs` gives you an easy way of specifying which environment variables to
look for as program arguments/config. This library aims at providing an as 
similar as possible experience to that of using Python's `argparse` package.

## Why envargs?
As most containerized applications take most or at least part of their 
runtime config from environment variables I always seemed to reimplement the 
same parsing utilities for new projects. `envargs.EnvParser` gives you a 
no-dependencies quick way of specifying which variables your program needs 
to run, which are mandatory and which are not, and their data types. A 
single call to `parse_env` and your registered environment variables are 
decoded, ready to be used.

## Usage
Registering interesting variables and getting a hold of them in your running
program is very straight forward:
```python
import envargs

NUM_WORKERS = "NUM_WORKERS"
RETENTION_POLICY = "RETENTION_POLICY"
RETENTION_POLICY_DEFAULT = "keep-alive"

parser = envargs.EnvParser()
parser.add_variable(NUM_WORKERS, type=int, default=1)
parser.add_variable(RETENTION_POLICY, default=RETENTION_POLICY_DEFAULT)
namespace = parser.parse_env()

num_workers = namespace.num_workers
retention_policy = namespace.retention_policy
...
```
Register interesting variables with `add_variable` and call `parse_env()` to 
obtain a populated namespace. The namespace will have one property per found 
*registered* variable, named the same as the environment variable in lower 
case. Variable `NUM_WORKERS` thus gets the field name `namespace.num_workers`.

### Generating a description text
In addition, to further clarify how your 
application uses environment variables, an `envargs.EnvParser` provides a 
helpful description property that can help determine if you have set up your 
variables as expected. After calling `add_variables` on your `EnvParser` 
instance for each interesting variable, the parsers `description` field will 
contain a helpful text which describes all added variables, if they're 
required, their types, and their default values (if any).

```python
import envargs
import logging

LOGGER = logging.getLogger(__name__)

env_parser = envargs.EnvParser()
env_parser.add_variable("NUM_WORKERS", type=int)
env_parser.add_variable("RETRY_COUNTER", required=False, type=int)
env_parser.add_variable("OVERLOAD_LIMIT", default="high")

LOGGER.debug(env_parser.description)
```
This text can be added to an `argparse.ArgumentParser` description to show 
the text easily from the commandline. Note the use of the raw formatter 
class to avoid stripping line breaks.
```python
import argparse
import envargs

env_parser = envargs.EnvParser()
env_parser.add_variable("WORK_DIST_STRATEGY", default="round-robin")
env_parser.add_variable("CPU_OVERLOAD_LIMIT_PERCENT", type=int, default=80)

arg_parser = argparse.ArgumentParser(
   formatter_class=argparse.RawDescriptionHelpFormatter,
   description="""
This is the best program, ever written, clearly.

Below, some info on environment variables used \/
""" + env_parser.description)
...
```

### Variable properties
An environment variable, when registered, can be given a set of properties, 
all available as keyword arguments given to `EnvParser.add_variable(...)`. 
If no keyword arguments are given, just the name of the environment variable 
itself, it will be assumed that:
1. The variable is required (raising a `RequiredError` in case it's missing)
2. The variable is a string (meaning no special decoding of the string 
   gotten from `os.getenv(...)`) is done
3. There is no default value

Read the docstring on `EnvParser.add_variable` to see all available options. 
Below is an example of a non-required boolean variable with a default value:

```python
import envargs

def do_throttling(acceptor_throttling: bool):
    ...

env_parser = envargs.EnvParser()
env_parser.add_variable("ACCEPTOR_THROTTLING", type=bool, required=False, 
                        default=False)
ns = env_parser.parse_env()

do_throttling(ns.acceptor_throttling)
```
The above will attempt to find the environment variable 
`ACCEPTOR_THROTTLING` and if it doesn't exist, fall back on `False` as a 
default value. The namespace returned by `parse_env` will therefore always 
be populated with the `acceptor_throttling` property.

An optional environment variable that isn't present at the time of calling 
`parse_env` will still yield an attribute in the returmed namespace, set to 
`None`.

```python
import envargs

env_parser = envargs.EnvParser()
env_parser.add_variable("OPTIONAL_SETTING", required=False)
namespace = env_parser.parse_env()

# Will not raise an assertion error
assert namespace.optional_setting is None
```

### Parsing booleans
To simplify things for this library, booleans can be expressed in only a few 
selected ways:
```python
TRUTH_SET = {"true", "True", "1"}
FALSE_SET = {"false", "False", "0"}
```
These definitions are available at `envargs.envargs.TRUTH_SET/FALSE_SET`. 
Deviating from these values for a boolean variable will result in a 
`ParseError`:

```python
import envargs
import os

from envargs.errors import ParseError

SYSTEM_ONLINE = "SYSTEM_ONLINE"

# Set the env var to something other than what's defined in the 
# TRUTH_SET/FALSE_SET
os.environ[SYSTEM_ONLINE] = "YES"

env_parser = envargs.EnvParser()
env_parser.add_variable(SYSTEM_ONLINE, type=bool)

# This statement raises a ParseError
try:
    env_parser.parse_env()
except ParseError as e:
    print(e)
```