service-configurator 1.0.0
A package for storing service config and secrets
License: MIT
Requires Python: >=3.8.1

# Service Configurator
[![Coverage Status](https://coveralls.io/repos/github/piotrekm7/service-configurator/badge.svg)](https://coveralls.io/github/piotrekm7/service-configurator)

Service Configurator is a python library created for managing settings and secrets for your service.

## Installation

Use package manager pip to install service-configurator.

```bash
pip install service-configurator
```

## Usage

### Creating settings classes

All settings classes should derive from BaseSettings or its subclass.

```python
from configurator import BaseSettings, Integer, String


class MySettings(BaseSettings):
    user_id = Integer()
    password = String()


config = MySettings()

config.user_id = 10
config.password = '123'

print(f'User: {config.user_id} logged with password: {config.password}')
```

Settings class could have other settings objects as members.

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var1 = Integer()


class MySettings2(BaseSettings):
    my_settings = MySettings()
    var2 = Integer()
```

Inheriting from another settings class is also possible. Subclass treats parent's settings as its own.

```python
from configurator import BaseSettings, Integer


class ParentSettings(BaseSettings):
    var1 = Integer()


class MySettings(ParentSettings):
    var2 = Integer()
```

equals

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var1 = Integer()
    var2 = Integer()
```

### Available settings fields

All implemented settings fields are presented below.

```python
from configurator import BaseSettings, Integer, PositiveInteger, String, Email, Boolean, Float, Url


class MySettings(BaseSettings):
    var1 = Integer()
    var2 = PositiveInteger()
    var3 = String()
    var4 = Email()
    var5 = Boolean()
    var6 = Float()
    var7 = Url()
```

Remember to always include parentheses () when creating settings fields.

### Optional fields and default values

Setting fields can be marked as optional using required parameter. In addition to that default value can be set, the
default value is returned if field wasn't modified. When these arguments are not provided field is required and it's
default value is field type specific.

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var = Integer(required=False, default=50)
```

### Importing settings

You can import settings using:

- python dict object
- json file
- yaml file

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var = Integer()


config = MySettings()

config.from_dict({'var': 12})
config.from_json('file.json')
config.from_yaml('file.yaml')
```

When importing from python dict object you can use partial_update argument if you don't want to provide all required
values, which could be useful when unit testing. By default, an exception is thrown if required values are missing. This
option is not accessible for json and yaml imports.

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var1 = Integer()
    var2 = Integer()


config = MySettings()

config.from_dict({'var1': 12}, partial_update=True)
```

### Exporting settings

Similar to import you have few export options:

- python dict object
- json file
- yaml file

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var = Integer()


config = MySettings()
config.var = 10

config.to_dict()  # {'var': 10}
config.to_json('file.json')
config.to_yaml('file.yaml')
```

Yaml files are recommended option for storing your configuration.

### Generating template files

Template/skeleton config file can be simply generated by creating a new instance of a settings class and using export
method.

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var = Integer()


config = MySettings()

config.to_json('file.json.skel')
config.to_yaml('file.yaml.skel')
```

### Setting and getting single field

If you want get or set single attribute you can access is as normal class member.

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var = Integer()


config = MySettings()

config.var = 4
print(config.var)  # prints 4
```

### Private settings fields

If you add a non-field member to your config class, it won't be exported or imported. However you can still use it as a
normal class member.

```python
from configurator import BaseSettings, Integer


class MySettings(BaseSettings):
    var = Integer(default=5)
    multiplier = 2

    def multiply(self):
        return self.var * self.multiplier


config = MySettings()
print(config.multiply())  # prints 10
config.to_dict()  # {'var': 5}
```

### Handling exceptions

Configurator throws following exceptions when something goes wrong:

- ValidationError - provided value didn't pass validation checks for the field
- SettingsError - a generic Settings error, read an error message for more information

```python
from configurator import BaseSettings, PositiveInteger, ValidationError, SettingsError


class MySettings(BaseSettings):
    var = PositiveInteger()


config = MySettings()

try:
    config.var = -1
except ValidationError as ex:
    # ValidationError -1 can't be assigned to PositiveInteger
    print(f'Validation error: {ex}')

try:
    config.from_dict({})
except SettingsError as ex:
    # SettingsError missing required field 'var'
    print(f'Settings error: {ex}')
```

Exceptions shouldn't pass silently.

### Utility classes

For commonly used sets of settings utility classes were implemented to avoid unnecessary code repetition in multiple
services.

All currently implemented utility classes are presented below. Check docs for class destiny.

```python
from configurator import BaseSettings
from configurator.utils import OracleConnectorSettings, BoxSettings


class MySettings(BaseSettings):
    oracle_db = OracleConnectorSettings()
    box = BoxSettings()
```

### Complete example

```python
from configurator import BaseSettings, String, Email
from configurator.utils import OracleConnectorSettings


class MySettings(BaseSettings):
    api_key = String()
    report_email = Email(required=False)
    oracle_db = OracleConnectorSettings()


config = MySettings()
config.from_yaml('config.yml')
```

#### `config.yaml`

```yaml
api_key: '123qwerty'
oracle_db:
  host: 'http://localhost'
  password: 'pass123'
  port: 1521
  sid: 'db2'
  user: 'admin'
report_email: 'report@example.com'

```

## Contributing

### Creating new settings fields

- All fields classes are located in the `fields.py` file.
- Every field should be inherited from `Field` class or its subclass.
- All fields should implement `default`, `type`, and `validate`, unless it's implemented in parent class and changes
  aren't needed

Example field classes:

```python
# implemented in fields.py

class String(Field):
    """
    Class for string type fields.
    """
    type_ = str
    default = ''


class Email(String):
    """
    Class for email fields.
    """

    def validate(self, value: str) -> str:
        """Validates email using simple regex."""
        value = super().validate(value)
        regex = re.compile(r'^\S+@\S+\.\S+$')
        if regex.fullmatch(value) is None:
            raise ValidationError('Provided string is not a valid email.')
        return value

```

### Creating new utility settings

Common sets of parameters used in many services shouldn't be copy-pasted. Instead, a new utility class should be created
in a `utils.py`. Create the utility class as a normal settings class.

```python
# implemented in utils.py

class OracleConnectorSettings(BaseSettings):
    """Setting required for connection to oracle database."""
    host = Url()
    port = Integer()
    user = String()
    password = String()
    sid = String()

    def get_connection_url(self):
        """Get connection url for sql alchemy"""
        return f"oracle://{self.user}:{self.password}@{self.host}:{self.port}/{self.sid}"
```

### Additional things to consider

- Every class and function should be documented. In addition to that run pydoc3 to generate html documentation after
  modifications.
  ```bash
  pdoc --html -o docs configurator
  ```
- Test coverage of this package is 100% try your best to not lower it.
- Update `__all__` in `__init__.py` if needed