Skip to content

Commit

Permalink
Merge pull request #5 from guionardo/feature/config
Browse files Browse the repository at this point in the history
Fixed tests and dict exporting
  • Loading branch information
guionardo authored Jun 28, 2022
2 parents 5c8502e + 3571df1 commit a1b8243
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 10 deletions.
166 changes: 166 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Tool classes and functions for Guiosoft projects
![PyPI - Downloads](https://img.shields.io/pypi/dm/py-gstools)
[![Pylint](https://github.com/guionardo/py-gstools/actions/workflows/pylint.yml/badge.svg)](https://github.com/guionardo/py-gstools/actions/workflows/pylint.yml)

## Installing

```bash
pip install py-gstools
```
## Cache

Cache wrapper for multiple providers.
Expand All @@ -32,4 +37,165 @@ cache.set(key='key',
value = cache.get('key')

print(value)
```

## DotEnv

Read environment variables from .env file

(Yeah, I know there is a py-dotenv package, but this is small and better to my needs)

Usage:

```python
import os
from gs.dotenv import load_env

# .env file
# CONFIGURATION_ONE=some_nasty_thing
# LOG_LEVEL=debug

load_env(verbose=True)
```

```bash
2022-06-28 15:59:05,052 INFO load_env(file_name=.env - {'CONFIGURATION_ONE': 'some_nasty_thing', 'LOG_LEVEL': 'debug'})
```

## Config

Configuration classes from files or environment variables

## Read data from environment variables

```python
from gs.config import BaseConfig

class EnvConfig(BaseConfig):
"""Environment based config"""

TESTING_ALPHA: str = 'alpha' # ENV:TEST_ALPHA
TESTING_BETA: str = 'beta'
TESTING_GAMMA: bool = False # ENV:TEST_GAMMA

# Here, the environment variables must be available by the OS (by os.environ)

# TESTING_ALPHA field maps to TEST_ALPHA environment var (defined in the comment)
# TESTING_BETA field maps to TESTING_BETA (default behavior, same to field name)
# TESTING_GAMMA fiels maps to TEST_GAMMA

cfg = EnvConfig.load_from_env()

print(cfg.sample_dict())

{'TESTING_BETA': 'beta', 'TEST_ALPHA': 'alpha', 'TEST_GAMMA': False}

```

## Read data from file

config.json file
```json
{
"TESTING_BETA": "beta",
"TEST_ALPHA": "alpha",
"TEST_GAMMA": true
}
```

config.yaml file
```yaml
TESTING_BETA: beta
TEST_ALPHA: alpha
TEST_GAMMA: true
```
```python
from gs.config import BaseConfig

class EnvConfig(BaseConfig):
"""Environment based config"""

TESTING_ALPHA: str = 'alpha' # ENV:TEST_ALPHA
TESTING_BETA: str = 'beta'
TESTING_GAMMA: bool = True # ENV:TEST_GAMMA

cfg = EnvConfig.load_from_file('config.json')

print(cfg.sample_dict())

{'TESTING_BETA': 'beta', 'TEST_ALPHA': 'alpha', 'TEST_GAMMA': True}

```

## Composite configurations

config.json file
```json
{
"INT_ARG": 2,
"LIST_ARG": ["1", "2", "3", "4"],
"STR_ARG": "1234ABCD",
"SUB_CONFIG": {
"ARG_1": 10,
"ARG_2": "abc"
},
"INT_ARG_2": 10,
"SUB_CONFIGS": [
{
"ARG_1": 2,
"ARG_2": "EFGH"
},
{
"ARG_1": 3,
"ARG_2": "IJKL"
}
]
}
```

config.yaml file
```yaml
INT_ARG: 2
INT_ARG_2: 10
LIST_ARG:
- '1'
- '2'
- '3'
- '4'
STR_ARG: 1234ABCD
SUB_CONFIG:
ARG_1: 10
ARG_2: ABCD
SUB_CONFIGS:
- ARG_1: 2
ARG_2: EFGH
- ARG_1: 3
ARG_2: IJKL
```
```python
from gs.config import BaseConfig

class SubConfig(BaseConfig):
"""Sample configuration class"""
ARG_1: int = 10
ARG_2: str = 'abc'


class Config(BaseConfig):
"""Sample configuration class"""

INT_ARG: int = 1 # DOCUMENT FOR INT_ARG
INT_ARG_2: int
STR_ARG = 'abcd' # DOCUMENT FOR STR_ARG
LIST_ARG: List[str] = ['a', 'b', 'c', 'd']
SUB_CONFIG: SubConfig
SUB_CONFIGS: List[SubConfig]

cfg = EnvConfig.load_from_file('config.json')

print(cfg.to_dict())

{'LIST_ARG': ['1', '2', '3', '4'], 'INT_ARG': 2, 'STR_ARG': '1234ABCD', 'SUB_CONFIG': {'ARG_2': 'abc', 'ARG_1': 2}, 'INT_ARG_2': 10, 'SUB_CONFIGS': [{'ARG_2': 'EFGH', 'ARG_1': 2}, {'ARG_2': 'IJKL', 'ARG_1': 3}]}
```
2 changes: 1 addition & 1 deletion gs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Package data"""
__version__ = '0.1.6'
__version__ = '0.1.7'
__tool_name__ = 'py-gstools'
__description__ = 'Tool classes and functions for Guiosoft projects'
__author__ = 'Guionardo Furlan'
Expand Down
4 changes: 3 additions & 1 deletion gs/cache/file_cache.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""File Cache"""
import datetime
import hashlib
import json
import logging
import os
Expand All @@ -17,7 +18,8 @@ def __init__(self) -> None:
self.path = None

def _filename(self, key: str) -> str:
return os.path.join(self.path, f'cache_{hash(key)}.json')
hash_name = hashlib.sha1(key.encode('utf-8')).hexdigest()
return os.path.join(self.path, f'cache_{hash_name}.json')

def get(self, key: str) -> Union[str, None]:
filename = self._filename(key)
Expand Down
54 changes: 49 additions & 5 deletions gs/config/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def __parse_value_type(value, value_type):

raise TypeError(f'Unknown type {value_type}')

def load(source: dict) -> any:
def load(source: dict, returns_field_name: bool = False) -> any:
if returns_field_name:
return field_name
if field_name in source:
source_value = source[field_name]
if not is_list:
Expand All @@ -46,7 +48,10 @@ def load(source: dict) -> any:
source_value = [__parse_value_type(v, field_type)
for v in source_value]
else:
source_value = default_value
if not is_list or isinstance(default_value, list):
source_value = default_value
else:
source_value = [default_value]

return source_value

Expand All @@ -72,6 +77,16 @@ def __init__(self, source=None, **dict_source):
self.__fields = self.__parse_fields()
self.__load(source or dict_source)

@classmethod
def load_from_env(cls) -> 'BaseConfig':
"""Load configuration from environment variables"""
return cls(**os.environ)

@classmethod
def load_from_file(cls, filename: str) -> 'BaseConfig':
"""Load configuration from file"""
return cls(filename)

def __parse_fields(self):

default_values = get_fields_default_values(self)
Expand Down Expand Up @@ -108,8 +123,10 @@ def __load(self, source):
setattr(self, member, field(source))

def __parse_value_type(self, value_type, value):
if value_type in (int, float, bool, str):
if value_type in (int, float, str):
return value_type(value)
if value_type == bool:
return value.lower()[0] in ('t', '1', 'y')

if value_type == datetime:
return datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
Expand Down Expand Up @@ -151,5 +168,32 @@ def __repr__(self) -> str:
f"{k}={repr(getattr(self,k))}" for k in self.__fields)
return f'{self.__class__.__name__}({fields})'

def __dict__(self) -> dict:
return
def sample_dict(self) -> dict:
"""Return a sample dictionary with all fields and default values"""
def _sample_dict(obj):
if isinstance(obj, BaseConfig):
return obj.sample_dict()
if isinstance(obj, list):
return [_sample_dict(v) for v in obj]
return obj
res = {}
for _, field in self.__fields.items():
res[field({}, True)] = _sample_dict(field({}))
return res
# return {
# field({}, True): _sample_dict(field({}))
# for _, field in self.__fields.items()
# }

def to_dict(self) -> dict:
"""Return a dictionary with all fields and values"""
def _to_dict(obj):
if isinstance(obj, BaseConfig):
return obj.to_dict()
if isinstance(obj, list):
return [_to_dict(v) for v in obj]
return obj
return {
field({}, True): _to_dict(getattr(self, member))
for member, field in self.__fields.items()
}
47 changes: 44 additions & 3 deletions tests/config/test_base_config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Configuration tests"""
import math
from typing import List
import os
import tempfile
import unittest

from datetime import datetime, date, timedelta
from datetime import date, datetime, timedelta
from typing import List

from gs.config import BaseConfig

Expand All @@ -25,6 +26,14 @@ class Config(BaseConfig):
SUB_CONFIGS: List[SubConfig]


class EnvConfig(BaseConfig):
"""Environment based config"""

TESTING_ALPHA: str = 'alpha' # ENV:TEST_ALPHA
TESTING_BETA: str = 'beta'
TESTING_GAMMA: bool = False # ENV:TEST_GAMMA


class ConfigTypes(BaseConfig):
INT_ARG: int = 1
FLOAT_ARG: float = 1.0
Expand Down Expand Up @@ -56,3 +65,35 @@ def test_config(self):
SUB_CONFIGS=[{'ARG_1': 2, 'ARG_2': 'EFGH'}, {'ARG_1': 3, 'ARG_2': 'IJKL'}])
self.assertEqual(cfg.INT_ARG, 2)
self.assertEqual(cfg.SUB_CONFIG.ARG_1, 1)


def test_sample(self):
cfg = Config()
expected = {'INT_ARG': 1,
'INT_ARG_2': 0,
'LIST_ARG': ['a', 'b', 'c', 'd'],
'STR_ARG': 'abcd',
'SUB_CONFIG': {'ARG_1': 10, 'ARG_2': 'abc'},
'SUB_CONFIGS': [{'ARG_1': 10, 'ARG_2': 'abc'}]}
self.assertDictEqual(expected, cfg.sample_dict())

def test_load_from_env(self):
os.environ.update({
'TEST_ALPHA': 'ALPHA',
'TESTING_BETA': 'BETA',
'TEST_GAMMA': '1'
})
cfg = EnvConfig.load_from_env()
self.assertEqual('ALPHA', cfg.TESTING_ALPHA)
self.assertEqual('BETA', cfg.TESTING_BETA)
self.assertTrue(cfg.TESTING_GAMMA)

def test_load_from_file(self):
with tempfile.NamedTemporaryFile('w', delete=True) as tmp:
tmp.write(
'{"TEST_ALPHA": "ALPHA", "TESTING_BETA": "BETA", "TEST_GAMMA": true}')
tmp.flush()
cfg = EnvConfig.load_from_file(tmp.name)
self.assertEqual('ALPHA', cfg.TESTING_ALPHA)
self.assertEqual('BETA', cfg.TESTING_BETA)
self.assertTrue(cfg.TESTING_GAMMA)

0 comments on commit a1b8243

Please sign in to comment.