nanoconf 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nanoconf-1.0.0/.github/workflows/codeql-analysis.yml +45 -0
- nanoconf-1.0.0/.github/workflows/python-publish.yml +35 -0
- nanoconf-1.0.0/.gitignore +5 -0
- nanoconf-1.0.0/.pre-commit-config.yaml +12 -0
- nanoconf-1.0.0/LICENSE +3 -0
- nanoconf-1.0.0/PKG-INFO +148 -0
- nanoconf-1.0.0/README.md +119 -0
- nanoconf-1.0.0/nanoconf/__init__.py +3 -0
- nanoconf-1.0.0/nanoconf/nanoconf.py +47 -0
- nanoconf-1.0.0/nanoconf.egg-info/PKG-INFO +148 -0
- nanoconf-1.0.0/nanoconf.egg-info/SOURCES.txt +23 -0
- nanoconf-1.0.0/nanoconf.egg-info/dependency_links.txt +1 -0
- nanoconf-1.0.0/nanoconf.egg-info/not-zip-safe +1 -0
- nanoconf-1.0.0/nanoconf.egg-info/requires.txt +13 -0
- nanoconf-1.0.0/nanoconf.egg-info/top_level.txt +1 -0
- nanoconf-1.0.0/pyproject.toml +127 -0
- nanoconf-1.0.0/setup.cfg +4 -0
- nanoconf-1.0.0/tests/data/animals/dog.nconf +6 -0
- nanoconf-1.0.0/tests/data/animals/elephant.nconf +5 -0
- nanoconf-1.0.0/tests/data/animals/lion.nconf +5 -0
- nanoconf-1.0.0/tests/data/animals/panda.nconf +5 -0
- nanoconf-1.0.0/tests/data/animals/penguin.nconf +5 -0
- nanoconf-1.0.0/tests/data/nested.nconf +9 -0
- nanoconf-1.0.0/tests/data/simple.nconf +8 -0
- nanoconf-1.0.0/tests/test_nanoconf.py +54 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: "CodeQL"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
types: [opened, synchronize, reopened]
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- "*.md"
|
|
10
|
+
- ".gitignore"
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
analyze:
|
|
14
|
+
name: CodeQL Analysis
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.11", "3.12"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout repository
|
|
24
|
+
uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Initialize CodeQL
|
|
27
|
+
uses: github/codeql-action/init@v2
|
|
28
|
+
with:
|
|
29
|
+
languages: ${{ matrix.language }}
|
|
30
|
+
|
|
31
|
+
- name: Perform CodeQL Analysis
|
|
32
|
+
uses: github/codeql-action/analyze@v2
|
|
33
|
+
|
|
34
|
+
- name: Setup Python
|
|
35
|
+
uses: actions/setup-python@v4
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
|
|
39
|
+
- name: Pre Commit Checks
|
|
40
|
+
uses: pre-commit/action@v3.0.0
|
|
41
|
+
|
|
42
|
+
- name: Unit Tests
|
|
43
|
+
run: |
|
|
44
|
+
pip install -U pip .[dev]
|
|
45
|
+
pytest -v tests/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: PythonPackage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
name: Build and Deploy to PyPi
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
# build/push in lowest support python version
|
|
15
|
+
python-version: [ "3.11" ]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v4
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Setup and Build
|
|
25
|
+
run: |
|
|
26
|
+
pip install -U pip .[setup]
|
|
27
|
+
python -m build
|
|
28
|
+
python -m twine check dist/*
|
|
29
|
+
|
|
30
|
+
- name: Build and publish
|
|
31
|
+
uses: pypa/gh-action-pypi-publish@v1.8.10
|
|
32
|
+
with:
|
|
33
|
+
user: __token__
|
|
34
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
35
|
+
skip_existing: true
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# configuration for pre-commit git hooks
|
|
2
|
+
repos:
|
|
3
|
+
- repo: https://github.com/psf/black
|
|
4
|
+
rev: 23.3.0
|
|
5
|
+
hooks:
|
|
6
|
+
- id: black
|
|
7
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
8
|
+
# Ruff version.
|
|
9
|
+
rev: v0.0.277
|
|
10
|
+
hooks:
|
|
11
|
+
- id: ruff
|
|
12
|
+
args: [--fix, --exit-non-zero-on-fix]
|
nanoconf-1.0.0/LICENSE
ADDED
nanoconf-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nanoconf
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The tiny opinionated config loader.
|
|
5
|
+
Author-email: Jacob J Callahan <jacob.callahan05@gmail.com>
|
|
6
|
+
Project-URL: Repository, https://github.com/JacobCallahan/nanoconf
|
|
7
|
+
Keywords: nanoconf,config,configuration,settings
|
|
8
|
+
Platform: any
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: python-box
|
|
19
|
+
Requires-Dist: pyyaml
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: black; extra == "dev"
|
|
22
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Provides-Extra: setup
|
|
26
|
+
Requires-Dist: build; extra == "setup"
|
|
27
|
+
Requires-Dist: setuptools; extra == "setup"
|
|
28
|
+
Requires-Dist: twine; extra == "setup"
|
|
29
|
+
|
|
30
|
+
NanoConf
|
|
31
|
+
========
|
|
32
|
+
NanoConf is a tiny, opinionated, and easy to use configuration library for Python. It is designed to be used in small to medium sized projects where a full blown configuration library is overkill.
|
|
33
|
+
|
|
34
|
+
Installation
|
|
35
|
+
------------
|
|
36
|
+
```bash
|
|
37
|
+
pip install nanoconf
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Usage
|
|
41
|
+
-----
|
|
42
|
+
```python
|
|
43
|
+
from nanoconf import NanoConf
|
|
44
|
+
# or if NanoConf if too long of a name
|
|
45
|
+
from nanoconf import NC
|
|
46
|
+
|
|
47
|
+
# Create a new configuration object
|
|
48
|
+
config = NanoConf("/path/to/config.nconf")
|
|
49
|
+
|
|
50
|
+
# Use the configuration object
|
|
51
|
+
print(config["key"])
|
|
52
|
+
# Since all values are also loaded as attributes, you can also do
|
|
53
|
+
print(config.key)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Configuration File Format
|
|
57
|
+
-------------------------
|
|
58
|
+
NanoConf uses a simple configuration file format that is easy to read and write.
|
|
59
|
+
Each File is YAML formatted and contains a single top-level dictionary.
|
|
60
|
+
Even though the top-level must be a dictionary, you can nest dictionaries and lists as deep as you want.
|
|
61
|
+
Each config file also must have the .nconf extension. This ensures that NanoConf will only load files that are meant to be configuration files.
|
|
62
|
+
```yaml
|
|
63
|
+
key: value
|
|
64
|
+
test: 1
|
|
65
|
+
overriden: false
|
|
66
|
+
things:
|
|
67
|
+
- thing1
|
|
68
|
+
- thing2
|
|
69
|
+
- thing3
|
|
70
|
+
top:
|
|
71
|
+
v1: 1
|
|
72
|
+
middle:
|
|
73
|
+
v2: 2
|
|
74
|
+
inner:
|
|
75
|
+
v3: 3
|
|
76
|
+
deep:
|
|
77
|
+
v4: 4
|
|
78
|
+
```
|
|
79
|
+
If you have multiple config files you want to load into a single config object, you can put them all in the same directory and pass that directory to NanoConf.
|
|
80
|
+
NanoConf will automatically place sub-files by their filename as an attribute of the parent file.
|
|
81
|
+
The contents of that file will be accessible as you'd expect under the corresponding filename attribute.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
<project root>
|
|
85
|
+
conf_dir
|
|
86
|
+
|__ cfg1.nconf
|
|
87
|
+
|__ cfg2.nconf
|
|
88
|
+
|__ cfg3.nconf
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# load an entire directory
|
|
93
|
+
proj_config = NanoConf("/path/to/conf_dir")
|
|
94
|
+
print(proj_config.cfg1.test)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or you can import additional files or directories from within any config file by using the `_import` keyword.
|
|
98
|
+
```yaml
|
|
99
|
+
# main.nconf
|
|
100
|
+
_import:
|
|
101
|
+
- /path/to/project/more_config
|
|
102
|
+
key: value
|
|
103
|
+
test: 1
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
<project root>
|
|
108
|
+
main.nconf
|
|
109
|
+
more_config
|
|
110
|
+
|__ subcfg1.nconf
|
|
111
|
+
|__ subcfg2.nconf
|
|
112
|
+
|__ subcfg3.nconf
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# loading the main config file will also load the sub-configs
|
|
117
|
+
proj_config = NanoConf("/path/to/project/main.nconf")
|
|
118
|
+
print(proj_config.more_config.subcfg1.test)
|
|
119
|
+
```
|
|
120
|
+
Notice how the directory structure was also maintained in the attribute path. This makes it easier to find the file that a value came from.
|
|
121
|
+
|
|
122
|
+
Environment Variables
|
|
123
|
+
---------------------
|
|
124
|
+
NanoConf supports environment variables either as overrides to existing values or as additions to the loaded config.
|
|
125
|
+
Envars are evaluated on a per-file basis, so you can have different envars for different config files.
|
|
126
|
+
The way we manage this is by having a special `_envar_prefix` key in the config file.
|
|
127
|
+
**Note:** NanoConf will not modify the case of any environment varable name or value. It is up to you to ensure that the case is correct.
|
|
128
|
+
```yaml
|
|
129
|
+
_envar_prefix: myapp
|
|
130
|
+
key: value
|
|
131
|
+
overrideme: original
|
|
132
|
+
```
|
|
133
|
+
```bash
|
|
134
|
+
export myapp_overrideme=changed
|
|
135
|
+
```
|
|
136
|
+
```python
|
|
137
|
+
config = NanoConf("/path/to/config.nconf")
|
|
138
|
+
print(config.overrideme)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
You can also pass complex data structures as JSON strings in environment variables.
|
|
142
|
+
```bash
|
|
143
|
+
export myapp_abc='{"a": 1, "b": 2, "c": 3}'
|
|
144
|
+
```
|
|
145
|
+
```python
|
|
146
|
+
config = NanoConf("/path/to/config.nconf")
|
|
147
|
+
print(config.abc.b)
|
|
148
|
+
```
|
nanoconf-1.0.0/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
NanoConf
|
|
2
|
+
========
|
|
3
|
+
NanoConf is a tiny, opinionated, and easy to use configuration library for Python. It is designed to be used in small to medium sized projects where a full blown configuration library is overkill.
|
|
4
|
+
|
|
5
|
+
Installation
|
|
6
|
+
------------
|
|
7
|
+
```bash
|
|
8
|
+
pip install nanoconf
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Usage
|
|
12
|
+
-----
|
|
13
|
+
```python
|
|
14
|
+
from nanoconf import NanoConf
|
|
15
|
+
# or if NanoConf if too long of a name
|
|
16
|
+
from nanoconf import NC
|
|
17
|
+
|
|
18
|
+
# Create a new configuration object
|
|
19
|
+
config = NanoConf("/path/to/config.nconf")
|
|
20
|
+
|
|
21
|
+
# Use the configuration object
|
|
22
|
+
print(config["key"])
|
|
23
|
+
# Since all values are also loaded as attributes, you can also do
|
|
24
|
+
print(config.key)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Configuration File Format
|
|
28
|
+
-------------------------
|
|
29
|
+
NanoConf uses a simple configuration file format that is easy to read and write.
|
|
30
|
+
Each File is YAML formatted and contains a single top-level dictionary.
|
|
31
|
+
Even though the top-level must be a dictionary, you can nest dictionaries and lists as deep as you want.
|
|
32
|
+
Each config file also must have the .nconf extension. This ensures that NanoConf will only load files that are meant to be configuration files.
|
|
33
|
+
```yaml
|
|
34
|
+
key: value
|
|
35
|
+
test: 1
|
|
36
|
+
overriden: false
|
|
37
|
+
things:
|
|
38
|
+
- thing1
|
|
39
|
+
- thing2
|
|
40
|
+
- thing3
|
|
41
|
+
top:
|
|
42
|
+
v1: 1
|
|
43
|
+
middle:
|
|
44
|
+
v2: 2
|
|
45
|
+
inner:
|
|
46
|
+
v3: 3
|
|
47
|
+
deep:
|
|
48
|
+
v4: 4
|
|
49
|
+
```
|
|
50
|
+
If you have multiple config files you want to load into a single config object, you can put them all in the same directory and pass that directory to NanoConf.
|
|
51
|
+
NanoConf will automatically place sub-files by their filename as an attribute of the parent file.
|
|
52
|
+
The contents of that file will be accessible as you'd expect under the corresponding filename attribute.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
<project root>
|
|
56
|
+
conf_dir
|
|
57
|
+
|__ cfg1.nconf
|
|
58
|
+
|__ cfg2.nconf
|
|
59
|
+
|__ cfg3.nconf
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# load an entire directory
|
|
64
|
+
proj_config = NanoConf("/path/to/conf_dir")
|
|
65
|
+
print(proj_config.cfg1.test)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or you can import additional files or directories from within any config file by using the `_import` keyword.
|
|
69
|
+
```yaml
|
|
70
|
+
# main.nconf
|
|
71
|
+
_import:
|
|
72
|
+
- /path/to/project/more_config
|
|
73
|
+
key: value
|
|
74
|
+
test: 1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
<project root>
|
|
79
|
+
main.nconf
|
|
80
|
+
more_config
|
|
81
|
+
|__ subcfg1.nconf
|
|
82
|
+
|__ subcfg2.nconf
|
|
83
|
+
|__ subcfg3.nconf
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# loading the main config file will also load the sub-configs
|
|
88
|
+
proj_config = NanoConf("/path/to/project/main.nconf")
|
|
89
|
+
print(proj_config.more_config.subcfg1.test)
|
|
90
|
+
```
|
|
91
|
+
Notice how the directory structure was also maintained in the attribute path. This makes it easier to find the file that a value came from.
|
|
92
|
+
|
|
93
|
+
Environment Variables
|
|
94
|
+
---------------------
|
|
95
|
+
NanoConf supports environment variables either as overrides to existing values or as additions to the loaded config.
|
|
96
|
+
Envars are evaluated on a per-file basis, so you can have different envars for different config files.
|
|
97
|
+
The way we manage this is by having a special `_envar_prefix` key in the config file.
|
|
98
|
+
**Note:** NanoConf will not modify the case of any environment varable name or value. It is up to you to ensure that the case is correct.
|
|
99
|
+
```yaml
|
|
100
|
+
_envar_prefix: myapp
|
|
101
|
+
key: value
|
|
102
|
+
overrideme: original
|
|
103
|
+
```
|
|
104
|
+
```bash
|
|
105
|
+
export myapp_overrideme=changed
|
|
106
|
+
```
|
|
107
|
+
```python
|
|
108
|
+
config = NanoConf("/path/to/config.nconf")
|
|
109
|
+
print(config.overrideme)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
You can also pass complex data structures as JSON strings in environment variables.
|
|
113
|
+
```bash
|
|
114
|
+
export myapp_abc='{"a": 1, "b": 2, "c": 3}'
|
|
115
|
+
```
|
|
116
|
+
```python
|
|
117
|
+
config = NanoConf("/path/to/config.nconf")
|
|
118
|
+
print(config.abc.b)
|
|
119
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from box import Box
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NanoConf:
|
|
11
|
+
def __init__(self, cfg_path="."):
|
|
12
|
+
self._cfg_path = Path(cfg_path)
|
|
13
|
+
self._name = self._cfg_path.stem
|
|
14
|
+
if self._cfg_path.is_dir():
|
|
15
|
+
for sub in self._cfg_path.glob("*.nconf"):
|
|
16
|
+
new_conf = NanoConf(sub)
|
|
17
|
+
setattr(self, new_conf._name, new_conf)
|
|
18
|
+
elif self._cfg_path.is_file() and self._cfg_path.suffix == ".nconf":
|
|
19
|
+
self._load_config()
|
|
20
|
+
self._pull_envars()
|
|
21
|
+
|
|
22
|
+
def _load_config(self):
|
|
23
|
+
cfg_dict = yaml.load(self._cfg_path.read_text(), Loader=yaml.SafeLoader)
|
|
24
|
+
for sub_import in cfg_dict.pop("_import", []):
|
|
25
|
+
new_conf = NanoConf(
|
|
26
|
+
self._cfg_path / sub_import
|
|
27
|
+
if self._cfg_path.is_dir()
|
|
28
|
+
else self._cfg_path.parent / sub_import
|
|
29
|
+
)
|
|
30
|
+
setattr(self, new_conf._name, new_conf)
|
|
31
|
+
self.__dict__.update(Box(cfg_dict))
|
|
32
|
+
|
|
33
|
+
def _pull_envars(self):
|
|
34
|
+
if getattr(self, "_envar_prefix", ""):
|
|
35
|
+
for key, val in os.environ.items():
|
|
36
|
+
if key.startswith(self._envar_prefix):
|
|
37
|
+
key_name = key.replace(f"{self._envar_prefix}_", "")
|
|
38
|
+
with contextlib.suppress(json.decoder.JSONDecodeError):
|
|
39
|
+
val = Box(json.loads(val)) # noqa: PLW2901
|
|
40
|
+
self.__dict__[key_name] = val
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return (
|
|
44
|
+
f"<NanoConf {self._name}.("
|
|
45
|
+
+ ", ".join(filter(lambda x: not x.startswith("_"), self.__dict__.keys()))
|
|
46
|
+
+ ")>"
|
|
47
|
+
)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nanoconf
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The tiny opinionated config loader.
|
|
5
|
+
Author-email: Jacob J Callahan <jacob.callahan05@gmail.com>
|
|
6
|
+
Project-URL: Repository, https://github.com/JacobCallahan/nanoconf
|
|
7
|
+
Keywords: nanoconf,config,configuration,settings
|
|
8
|
+
Platform: any
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: python-box
|
|
19
|
+
Requires-Dist: pyyaml
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: black; extra == "dev"
|
|
22
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Provides-Extra: setup
|
|
26
|
+
Requires-Dist: build; extra == "setup"
|
|
27
|
+
Requires-Dist: setuptools; extra == "setup"
|
|
28
|
+
Requires-Dist: twine; extra == "setup"
|
|
29
|
+
|
|
30
|
+
NanoConf
|
|
31
|
+
========
|
|
32
|
+
NanoConf is a tiny, opinionated, and easy to use configuration library for Python. It is designed to be used in small to medium sized projects where a full blown configuration library is overkill.
|
|
33
|
+
|
|
34
|
+
Installation
|
|
35
|
+
------------
|
|
36
|
+
```bash
|
|
37
|
+
pip install nanoconf
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Usage
|
|
41
|
+
-----
|
|
42
|
+
```python
|
|
43
|
+
from nanoconf import NanoConf
|
|
44
|
+
# or if NanoConf if too long of a name
|
|
45
|
+
from nanoconf import NC
|
|
46
|
+
|
|
47
|
+
# Create a new configuration object
|
|
48
|
+
config = NanoConf("/path/to/config.nconf")
|
|
49
|
+
|
|
50
|
+
# Use the configuration object
|
|
51
|
+
print(config["key"])
|
|
52
|
+
# Since all values are also loaded as attributes, you can also do
|
|
53
|
+
print(config.key)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Configuration File Format
|
|
57
|
+
-------------------------
|
|
58
|
+
NanoConf uses a simple configuration file format that is easy to read and write.
|
|
59
|
+
Each File is YAML formatted and contains a single top-level dictionary.
|
|
60
|
+
Even though the top-level must be a dictionary, you can nest dictionaries and lists as deep as you want.
|
|
61
|
+
Each config file also must have the .nconf extension. This ensures that NanoConf will only load files that are meant to be configuration files.
|
|
62
|
+
```yaml
|
|
63
|
+
key: value
|
|
64
|
+
test: 1
|
|
65
|
+
overriden: false
|
|
66
|
+
things:
|
|
67
|
+
- thing1
|
|
68
|
+
- thing2
|
|
69
|
+
- thing3
|
|
70
|
+
top:
|
|
71
|
+
v1: 1
|
|
72
|
+
middle:
|
|
73
|
+
v2: 2
|
|
74
|
+
inner:
|
|
75
|
+
v3: 3
|
|
76
|
+
deep:
|
|
77
|
+
v4: 4
|
|
78
|
+
```
|
|
79
|
+
If you have multiple config files you want to load into a single config object, you can put them all in the same directory and pass that directory to NanoConf.
|
|
80
|
+
NanoConf will automatically place sub-files by their filename as an attribute of the parent file.
|
|
81
|
+
The contents of that file will be accessible as you'd expect under the corresponding filename attribute.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
<project root>
|
|
85
|
+
conf_dir
|
|
86
|
+
|__ cfg1.nconf
|
|
87
|
+
|__ cfg2.nconf
|
|
88
|
+
|__ cfg3.nconf
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# load an entire directory
|
|
93
|
+
proj_config = NanoConf("/path/to/conf_dir")
|
|
94
|
+
print(proj_config.cfg1.test)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or you can import additional files or directories from within any config file by using the `_import` keyword.
|
|
98
|
+
```yaml
|
|
99
|
+
# main.nconf
|
|
100
|
+
_import:
|
|
101
|
+
- /path/to/project/more_config
|
|
102
|
+
key: value
|
|
103
|
+
test: 1
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
<project root>
|
|
108
|
+
main.nconf
|
|
109
|
+
more_config
|
|
110
|
+
|__ subcfg1.nconf
|
|
111
|
+
|__ subcfg2.nconf
|
|
112
|
+
|__ subcfg3.nconf
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# loading the main config file will also load the sub-configs
|
|
117
|
+
proj_config = NanoConf("/path/to/project/main.nconf")
|
|
118
|
+
print(proj_config.more_config.subcfg1.test)
|
|
119
|
+
```
|
|
120
|
+
Notice how the directory structure was also maintained in the attribute path. This makes it easier to find the file that a value came from.
|
|
121
|
+
|
|
122
|
+
Environment Variables
|
|
123
|
+
---------------------
|
|
124
|
+
NanoConf supports environment variables either as overrides to existing values or as additions to the loaded config.
|
|
125
|
+
Envars are evaluated on a per-file basis, so you can have different envars for different config files.
|
|
126
|
+
The way we manage this is by having a special `_envar_prefix` key in the config file.
|
|
127
|
+
**Note:** NanoConf will not modify the case of any environment varable name or value. It is up to you to ensure that the case is correct.
|
|
128
|
+
```yaml
|
|
129
|
+
_envar_prefix: myapp
|
|
130
|
+
key: value
|
|
131
|
+
overrideme: original
|
|
132
|
+
```
|
|
133
|
+
```bash
|
|
134
|
+
export myapp_overrideme=changed
|
|
135
|
+
```
|
|
136
|
+
```python
|
|
137
|
+
config = NanoConf("/path/to/config.nconf")
|
|
138
|
+
print(config.overrideme)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
You can also pass complex data structures as JSON strings in environment variables.
|
|
142
|
+
```bash
|
|
143
|
+
export myapp_abc='{"a": 1, "b": 2, "c": 3}'
|
|
144
|
+
```
|
|
145
|
+
```python
|
|
146
|
+
config = NanoConf("/path/to/config.nconf")
|
|
147
|
+
print(config.abc.b)
|
|
148
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
.pre-commit-config.yaml
|
|
3
|
+
LICENSE
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
.github/workflows/codeql-analysis.yml
|
|
7
|
+
.github/workflows/python-publish.yml
|
|
8
|
+
nanoconf/__init__.py
|
|
9
|
+
nanoconf/nanoconf.py
|
|
10
|
+
nanoconf.egg-info/PKG-INFO
|
|
11
|
+
nanoconf.egg-info/SOURCES.txt
|
|
12
|
+
nanoconf.egg-info/dependency_links.txt
|
|
13
|
+
nanoconf.egg-info/not-zip-safe
|
|
14
|
+
nanoconf.egg-info/requires.txt
|
|
15
|
+
nanoconf.egg-info/top_level.txt
|
|
16
|
+
tests/test_nanoconf.py
|
|
17
|
+
tests/data/nested.nconf
|
|
18
|
+
tests/data/simple.nconf
|
|
19
|
+
tests/data/animals/dog.nconf
|
|
20
|
+
tests/data/animals/elephant.nconf
|
|
21
|
+
tests/data/animals/lion.nconf
|
|
22
|
+
tests/data/animals/panda.nconf
|
|
23
|
+
tests/data/animals/penguin.nconf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nanoconf
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nanoconf"
|
|
3
|
+
description = "The tiny opinionated config loader."
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
keywords = ["nanoconf", "config", "configuration", "settings"]
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Jacob J Callahan", email = "jacob.callahan05@gmail.com"}
|
|
9
|
+
]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Natural Language :: English",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"python-box",
|
|
20
|
+
"pyyaml",
|
|
21
|
+
]
|
|
22
|
+
dynamic = ["version"] # dynamic fields to update on build - version via setuptools_scm
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/JacobCallahan/nanoconf"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools", "setuptools-scm[toml]", "wheel"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
platforms = ["any"]
|
|
33
|
+
zip-safe = false
|
|
34
|
+
include-package-data = true
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["nanoconf"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools_scm] # same as use_scm_version=True in setup.py
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = [
|
|
43
|
+
"black",
|
|
44
|
+
"pre-commit",
|
|
45
|
+
"pytest",
|
|
46
|
+
"ruff",
|
|
47
|
+
]
|
|
48
|
+
setup = [
|
|
49
|
+
"build",
|
|
50
|
+
"setuptools",
|
|
51
|
+
"twine",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = ["-v", "-l", "--color=yes", "--code-highlight=yes"]
|
|
57
|
+
|
|
58
|
+
[tool.black]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = ["py311"]
|
|
61
|
+
include = '\.pyi?$'
|
|
62
|
+
exclude = '''
|
|
63
|
+
/(
|
|
64
|
+
\.git
|
|
65
|
+
| \.venv
|
|
66
|
+
| build
|
|
67
|
+
| dist
|
|
68
|
+
| tests/data
|
|
69
|
+
)/
|
|
70
|
+
'''
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
target-version = "py311"
|
|
74
|
+
fixable = ["ALL"]
|
|
75
|
+
|
|
76
|
+
select = [
|
|
77
|
+
"B", # bugbear
|
|
78
|
+
"C", # complexity
|
|
79
|
+
"C4", # flake8-comprehensions
|
|
80
|
+
"COM818", # Trailing comma on bare tuple prohibited
|
|
81
|
+
"E", # pycodestyle
|
|
82
|
+
"F", # pyflakes/autoflake
|
|
83
|
+
"G", # flake8-logging-format
|
|
84
|
+
"I", # isort
|
|
85
|
+
"ISC001", # Implicitly concatenated string literals on one License
|
|
86
|
+
"N",
|
|
87
|
+
"PERF", # Perflint rules
|
|
88
|
+
"PGH004", # Use specific rule codes when using noqa
|
|
89
|
+
"PLC", # pylint
|
|
90
|
+
"PLE", # pylint
|
|
91
|
+
"PLR", # pylint
|
|
92
|
+
"PLW", # pylint
|
|
93
|
+
"PTH", # Use pathlib
|
|
94
|
+
"RUF", # Ruff-specific rules
|
|
95
|
+
"S", # flake8-bandit
|
|
96
|
+
"SIM", #flake8-simplify
|
|
97
|
+
"T100", # leftover breakpoint()
|
|
98
|
+
"T20", # leftover print()
|
|
99
|
+
"TRY004", # Prefer TypeError exception for invalid type
|
|
100
|
+
"TRY200", # Use raise from to specify exception cause
|
|
101
|
+
"TRY302", # Remove exception handler; error is immediately re-raised
|
|
102
|
+
"PL", # pylint
|
|
103
|
+
"UP", # pyupgrade
|
|
104
|
+
"W", # pycodestyle
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
ignore = [
|
|
108
|
+
"ANN", # flake8-annotations
|
|
109
|
+
"E501", # line too long
|
|
110
|
+
"RUF012", # Mutable class attributes should be annotated with typing.ClassVar
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
[tool.ruff.per-file-ignores]
|
|
114
|
+
"tests/test_nanoconf.py" = ["S101", "PLR2004"]
|
|
115
|
+
|
|
116
|
+
[tool.ruff.flake8-pytest-style]
|
|
117
|
+
fixture-parentheses = false
|
|
118
|
+
|
|
119
|
+
[tool.ruff.isort]
|
|
120
|
+
force-sort-within-sections = true
|
|
121
|
+
known-first-party = [
|
|
122
|
+
"nanoconf",
|
|
123
|
+
]
|
|
124
|
+
combine-as-imports = true
|
|
125
|
+
|
|
126
|
+
[tool.ruff.mccabe]
|
|
127
|
+
max-complexity = 25
|
nanoconf-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from box import Box, BoxList
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from nanoconf import NC
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def set_envars(request):
|
|
11
|
+
if isinstance(request.param, list):
|
|
12
|
+
for pair in request.param:
|
|
13
|
+
os.environ[pair[0]] = pair[1]
|
|
14
|
+
yield
|
|
15
|
+
for pair in request.param:
|
|
16
|
+
del os.environ[pair[0]]
|
|
17
|
+
else:
|
|
18
|
+
os.environ[request.param[0]] = request.param[1]
|
|
19
|
+
yield
|
|
20
|
+
del os.environ[request.param[0]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize("set_envars", [("simple_overriden", "True")], indirect=True)
|
|
24
|
+
def test_simple(set_envars):
|
|
25
|
+
nconf = NC("tests/data/simple.nconf")
|
|
26
|
+
assert nconf._name == "simple"
|
|
27
|
+
assert nconf._envar_prefix == "simple"
|
|
28
|
+
assert isinstance(nconf.things, BoxList)
|
|
29
|
+
assert len(nconf.things) == 3
|
|
30
|
+
assert nconf.overriden == "True"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize("set_envars", [("simple_overriden", '{"a": 1, "b": 2}')], indirect=True)
|
|
34
|
+
def test_json_envar(set_envars):
|
|
35
|
+
nconf = NC("tests/data/simple.nconf")
|
|
36
|
+
assert isinstance(nconf.overriden, Box)
|
|
37
|
+
assert nconf.overriden.a == 1
|
|
38
|
+
assert nconf.overriden.b == 2
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_nested():
|
|
42
|
+
nconf = NC("tests/data/nested.nconf")
|
|
43
|
+
assert nconf._name == "nested"
|
|
44
|
+
assert nconf.top.v1 == 1
|
|
45
|
+
assert nconf.top.middle.v2 == 2
|
|
46
|
+
assert nconf.top.middle.lowest.v3 == 3
|
|
47
|
+
assert nconf.animals.panda.diet == "Herbivore"
|
|
48
|
+
assert all([nconf.animals.dog, nconf.animals.lion, nconf.animals.penguin])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize("set_envars", [("testdog_diet", "Treats!")], indirect=True)
|
|
52
|
+
def test_nested_envar(set_envars):
|
|
53
|
+
nconf = NC("tests/data/nested.nconf")
|
|
54
|
+
assert nconf.animals.dog.diet == "Treats!"
|