-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from DustinMoriarty/feature/injector
Feature/injector
- Loading branch information
Showing
13 changed files
with
353 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
[flake8] | ||
max-line-length = 88 | ||
ignore = E501, E203, W503 | ||
per-file-ignores = __init__.py:F401 | ||
exclude = | ||
.git | ||
__pycache__ | ||
setup.py | ||
build | ||
dist | ||
releases | ||
.venv | ||
.tox | ||
.mypy_cache | ||
.pytest_cache | ||
.vscode | ||
.github |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,54 @@ | ||
# config-injector | ||
A simple json configuration dependency injector framework. | ||
Config-injector is a very simple framework which aims to do only two things: (1) define configurable functions and (2) inject configuration data into those functions at runtime. | ||
|
||
## Installation | ||
Install with pip. | ||
```bash | ||
pip install config-injector | ||
``` | ||
|
||
## Getting Started | ||
Annotate any callable as a configurable function using `@config`. Note that the `@config` decorator requires that you provide callable functions for each argument. These callable functions should return the expected type. The object is to break all arguments down to fundamental types: string, integer, float or dictionary. | ||
|
||
```python | ||
from collections import namedtuple | ||
from typing import Text, Dict, SupportsInt | ||
from pathlib import Path | ||
|
||
from config_injector import config, Injector | ||
|
||
|
||
MockThing0 = namedtuple("MockThing0", ["arg_1", "arg_2", "arg_3", "arg_4"]) | ||
|
||
@config(arg_1=str, arg_2=str, arg_3=str, arg_4=str) | ||
def mock_thing_0(arg_1: Text, arg_2: Text, arg_3: Text, arg_4: Text): | ||
return MockThing0(arg_1, arg_2, arg_3, arg_4) | ||
|
||
|
||
@config(arg_5=int, arg_6=int, arg_7=int, arg_8=int) | ||
def mock_thing_1(arg_5, arg_6, arg_7, arg_8): | ||
return {"key_a": arg_5, "key_b": arg_6, "key_c": arg_7, "key_d": arg_8} | ||
|
||
@config(t0=mock_thing_0, t1=mock_thing_1, arg_9=str) | ||
def mock_things(t0: MockThing0, t1: Dict[SupportsInt], arg_9: Text): | ||
return (t0, t1, arg_9) | ||
|
||
def get_things(config_file=Path("config.json")): | ||
injector = Injector() | ||
injector.load_file(config_file) | ||
return injector["things"].instantiate(mock_things) | ||
``` | ||
|
||
Now that the configurable functions are annotated, we can write a configuration for them. | ||
|
||
```json | ||
{ | ||
"things": { | ||
"t0": {"arg_1": "a", "arg_2": "b", "arg_3": "c", "arg_4": "d"}, | ||
"t1": {"arg_5": 1, "arg_6": 2, "arg_7": 3, "arg_8": 4}, | ||
"arg_9": "e" | ||
} | ||
} | ||
``` | ||
|
||
This configuration file can be loaded in the runtime portion of our implementation using `get_things()` to instantiate the configured objects created by our functions. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
try: | ||
from importlib.metadata import version as get_version | ||
except ImportError: | ||
from importlib_metadata import version as get_version | ||
|
||
from config_injector.config import config | ||
from config_injector.injector import Injector | ||
|
||
|
||
__version__ = get_version(__package__) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import json | ||
|
||
from collections.abc import MutableMapping | ||
from pathlib import Path | ||
from typing import Dict | ||
from typing import Text | ||
|
||
import toml | ||
import yaml | ||
|
||
from config_injector.config import SupportsFill | ||
from config_injector.config import fill | ||
from config_injector.exc import FileTypeNotRecognized | ||
|
||
|
||
class Injector(MutableMapping): | ||
def __init__(self, context: Dict = None): | ||
self.context = {} if context is None else context | ||
|
||
def __getitem__(self, k: Text): | ||
v = self.context[k] | ||
if hasattr(v, "items"): | ||
return self.__class__(v) | ||
else: | ||
return v | ||
|
||
def __iter__(self): | ||
return iter(self.context) | ||
|
||
def __setitem__(self, k, v): | ||
self.context[k] = v | ||
|
||
def __delitem__(self, k): | ||
self.context.pop(k) | ||
|
||
def __len__(self): | ||
return len(self.context) | ||
|
||
def clear(self): | ||
self.context = {} | ||
|
||
def load(self, context: Dict): | ||
self.context.update(context) | ||
|
||
def load_file(self, file: Path): | ||
if file.name.lower().endswith(".json"): | ||
self._load_json_file(file) | ||
elif file.name.lower().endswith(".toml"): | ||
self._load_toml_file(file) | ||
elif file.name.lower().endswith(".yaml") or file.name.lower().endswith(".yml"): | ||
self._load_yaml_file(file) | ||
else: | ||
raise FileTypeNotRecognized( | ||
f"Unable to determine file type for {file.name}" | ||
) | ||
|
||
def _load_json_file(self, file: Path): | ||
with file.open() as f: | ||
self.load(json.load(f)) | ||
|
||
def _load_toml_file(self, file: Path): | ||
with file.open() as f: | ||
self.load(toml.load(f)) | ||
|
||
def _load_yaml_file(self, file: Path): | ||
with file.open() as f: | ||
self.load(yaml.load(f, Loader=yaml.SafeLoader)) | ||
|
||
def instantiate(self, config: SupportsFill): | ||
return fill(config, self.context) |
File renamed without changes.
Oops, something went wrong.