Skip to content

Commit 96b0c59

Browse files
authored
Add interfaces and env dict/list parsing (#25) #minor
* add more interface and env dict/list parsing * fix bad renaming * fix tests * change publishing system with auto semver * fix poetry dev version parsing
1 parent 7799672 commit 96b0c59

12 files changed

+311
-51
lines changed

.flake8

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[flake8]
2-
ignore = E501
2+
ignore = E501, E203
33
import-order-style = google

.github/workflows/ci.yaml

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
name: CI
22

3-
on: [push, pull_request]
3+
on:
4+
- push
5+
- pull_request
46

57
jobs:
68

@@ -30,22 +32,37 @@ jobs:
3032
3133
publish:
3234
needs: test
33-
if: startsWith(github.ref, 'refs/tags/v')
35+
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
3436
runs-on: ubuntu-latest
3537

3638
steps:
3739
- uses: actions/checkout@v2
38-
- name: Set up Python ${{ matrix.python-version }}
40+
with:
41+
fetch-depth: '0'
42+
- name: Set up Python
3943
uses: actions/setup-python@v2
4044
with:
41-
python-version: 3.9
45+
python-version: 3.x
4246
- name: Install poetry
4347
uses: abatilo/actions-poetry@v2.0.0
48+
- name: Bump version and push tag
49+
id: version
50+
uses: anothrNick/github-tag-action@1.36.0
51+
env:
52+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53+
WITH_V: true
4454
- name: Publish
4555
env:
4656
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
57+
NEW_TAG: ${{ steps.version.outputs.new_tag }}
4758
run: |
4859
python -m pip install --upgrade pip
4960
poetry install
61+
poetry version $NEW_TAG
5062
poetry build
5163
poetry publish
64+
- name: Create a GitHub release
65+
uses: ncipollo/release-action@v1
66+
with:
67+
tag: ${{ steps.version.outputs.new_tag }}
68+
name: Release ${{ steps.version.outputs.new_tag }}

.pre-commit-config.yaml

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
repos:
22
- repo: https://github.com/sqlalchemyorg/zimports/
3-
rev: 0.3.0
3+
rev: v0.4.1
44
hooks:
55
- id: zimports
66
- repo: https://github.com/psf/black
7-
rev: 21.5b0
7+
rev: 21.9b0
88
hooks:
99
- id: black
1010
- repo: https://gitlab.com/pycqa/flake8
11-
rev: 3.9.1
11+
rev: 3.9.2
1212
hooks:
1313
- id: flake8
14-
- repo: https://github.com/zifeo/pre-commit
15-
rev: v0.1.4
16-
hooks:
17-
- id: poetry-tag-checker
14+
1815

README.md

+53
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ str_name = test
3636
str_name = ${?HOSTNAME}
3737
dash-to-underscore = true
3838
float_num = 2.2
39+
# this is a comment
3940
list_data = [
4041
a
4142
b
@@ -120,6 +121,11 @@ conf = dataconf.loads(str_conf, Base)
120121
```python
121122
import dataconf
122123

124+
conf = dataconf.string('{ name: Test }', Config)
125+
conf = dataconf.env('PREFIX_', Config)
126+
conf = dataconf.url('https://github.com/zifeo/dataconf/blob/master/.pre-commit-config.yaml', Config)
127+
conf = dataconf.file('confs/test.{hocon,json,yaml,properties}', Config)
128+
123129
conf = dataconf.loads('confs/test.hocon', Config)
124130
conf = dataconf.loads('confs/test.json', Config)
125131
conf = dataconf.loads('confs/test.yaml', Config)
@@ -134,6 +140,53 @@ dataconf.dumps('confs/test.properties', out='properties')
134140
Follows same api as python JSON package (e.g. `load`, `loads`, `dump`, `dumps`).
135141
For full HOCON capabilities see [here](https://github.com/chimpler/pyhocon/#example-of-hocon-file).
136142

143+
## Env dict/list parsing
144+
145+
```
146+
PREFIX_VAR=a
147+
PREFIX_VAR_NAME=b
148+
PREFIX_TEST__NAME=c
149+
PREFIX_LS_0=d
150+
PREFIX_LS_1=e
151+
PREFIX_LSLS_0_0=f
152+
PREFIX_LSOB_0__NAME=g
153+
PREFIX_NESTED="{ name: Test }"
154+
PREFIX_SUB="{ value: ${PREFIX_VAR} }"
155+
```
156+
157+
is equivalent to
158+
159+
```
160+
{
161+
var = a
162+
var_name = b
163+
test {
164+
name = c
165+
}
166+
ls = [
167+
d
168+
e
169+
]
170+
lsls = [
171+
[
172+
f
173+
]
174+
]
175+
lsob = [
176+
{
177+
name = g
178+
}
179+
]
180+
nested {
181+
name: Test
182+
}
183+
sub {
184+
# will have value "a" at parsing, useful for aliases
185+
value = ${PREFIX_VAR}
186+
}
187+
}
188+
```
189+
137190
## CI
138191

139192
```shell

dataconf/__init__.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
from dataconf.utils import dump
2-
from dataconf.utils import dumps
3-
from dataconf.utils import load
4-
from dataconf.utils import loads
1+
from dataconf.main import dump
2+
from dataconf.main import dumps
3+
from dataconf.main import env
4+
from dataconf.main import file
5+
from dataconf.main import load
6+
from dataconf.main import loads
7+
from dataconf.main import string
8+
from dataconf.main import url
59
from dataconf.version import __version__
610

7-
__all__ = ["load", "loads", "dump", "dumps", "__version__"]
11+
__all__ = [
12+
"load",
13+
"loads",
14+
"dump",
15+
"dumps",
16+
"env",
17+
"url",
18+
"file",
19+
"string",
20+
"__version__",
21+
]

dataconf/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ class MalformedConfigException(Exception):
1212

1313
class UnexpectedKeysException(Exception):
1414
pass
15+
16+
17+
class EnvListOrderException(Exception):
18+
pass

dataconf/main.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
3+
from dataconf import utils
4+
from dataconf.exceptions import MalformedConfigException
5+
from pyhocon import ConfigFactory
6+
from pyhocon import HOCONConverter
7+
import pyparsing
8+
9+
10+
def __parse_config_tree(conf, clazz):
11+
try:
12+
return utils.__parse(conf, clazz, "")
13+
except pyparsing.ParseSyntaxException as e:
14+
raise MalformedConfigException(
15+
f'parsing failure line {e.lineno} character {e.col}, got "{e.line}"'
16+
)
17+
18+
19+
def env(prefix: str, clazz):
20+
conf = ConfigFactory.from_dict(utils.__dict_list_parsing(prefix, os.environ))
21+
return __parse_config_tree(conf, clazz)
22+
23+
24+
def string(s: str, clazz):
25+
conf = ConfigFactory.parse_string(s)
26+
return __parse_config_tree(conf, clazz)
27+
28+
29+
def url(uri: str, clazz):
30+
conf = ConfigFactory.parse_URL(uri)
31+
return __parse_config_tree(conf, clazz)
32+
33+
34+
def file(path: str, clazz):
35+
conf = ConfigFactory.parse_file(path)
36+
return __parse_config_tree(conf, clazz)
37+
38+
39+
def load(path: str, clazz):
40+
return file(path, clazz)
41+
42+
43+
def loads(s: str, clazz):
44+
return string(s, clazz)
45+
46+
47+
def dump(file: str, instance: object, out: str):
48+
with open(file, "w") as f:
49+
f.write(dumps(instance, out=out))
50+
51+
52+
def dumps(instance: object, out: str):
53+
conf = utils.__generate(instance, "")
54+
55+
if out:
56+
if out.lower() == "hocon":
57+
return HOCONConverter.to_hocon(conf)
58+
if out.lower() == "yaml":
59+
return HOCONConverter.to_yaml(conf)
60+
if out.lower() == "json":
61+
return HOCONConverter.to_json(conf)
62+
if out.lower() == "properties":
63+
return HOCONConverter.to_properties(conf)
64+
65+
return conf

dataconf/utils.py

+65-32
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66
from typing import get_origin
77
from typing import Union
88

9+
from dataconf.exceptions import EnvListOrderException
910
from dataconf.exceptions import MalformedConfigException
1011
from dataconf.exceptions import MissingTypeException
1112
from dataconf.exceptions import TypeConfigException
1213
from dataconf.exceptions import UnexpectedKeysException
1314
from dateutil.relativedelta import relativedelta
1415
from pyhocon import ConfigFactory
15-
from pyhocon import HOCONConverter
1616
from pyhocon.config_tree import ConfigList
1717
from pyhocon.config_tree import ConfigTree
1818
import pyparsing
1919

20+
2021
NoneType = type(None)
2122

2223

@@ -181,42 +182,74 @@ def __generate(value: object, path):
181182
return value
182183

183184

184-
def load(file: str, clazz):
185-
try:
186-
conf = ConfigFactory.parse_file(file)
187-
return __parse(conf, clazz, "")
188-
except pyparsing.ParseSyntaxException as e:
189-
raise MalformedConfigException(
190-
f'parsing failure line {e.lineno} character {e.col}, got "{e.line}"'
191-
)
185+
def __dict_list_parsing(prefix: str, obj):
186+
ret = {}
192187

188+
def set_lens(p, focus, v):
193189

194-
def loads(string: str, clazz):
195-
try:
196-
conf = ConfigFactory.parse_string(string)
197-
return __parse(conf, clazz, "")
198-
except pyparsing.ParseSyntaxException as e:
199-
raise MalformedConfigException(
200-
f'parsing failure line {e.lineno} character {e.col}, got "{e.line}"'
201-
)
190+
# value
191+
if len(p) == 1:
192+
# []x
193+
if isinstance(focus, list):
194+
if p[0] != len(focus):
195+
raise EnvListOrderException
196+
focus.append(v)
197+
# {}x
198+
else:
199+
focus[p[0]] = v
200+
return
201+
202+
# dict
203+
if p[1] == "":
204+
205+
if p[0] not in focus:
206+
# []{x}
207+
if isinstance(focus, list):
208+
if p[0] != len(focus):
209+
raise EnvListOrderException
210+
focus.append({})
211+
# {}{x}
212+
else:
213+
focus[p[0]] = {}
202214

215+
return set_lens(p[2:], focus[p[0]], v)
203216

204-
def dump(file: str, instance: object, out: str):
205-
with open(file, "w") as f:
206-
f.write(dumps(instance, out=out))
217+
# list
218+
if isinstance(p[1], int):
207219

220+
if p[0] not in focus:
221+
# [][x]
222+
if isinstance(focus, list):
223+
if p[1] != len(focus):
224+
raise EnvListOrderException
225+
focus.append([])
226+
# {}[x]
227+
else:
228+
focus[p[0]] = []
208229

209-
def dumps(instance: object, out: str):
210-
conf = __generate(instance, "")
230+
return set_lens(p[1:], focus[p[0]], v)
231+
232+
# compose path
233+
return set_lens([f"{p[0]}_{p[1]}"] + p[2:], focus, v)
234+
235+
def int_or_string(v):
236+
try:
237+
return int(v)
238+
except ValueError:
239+
return v
240+
241+
if not prefix.endswith("_"):
242+
prefix = f"{prefix}_"
243+
244+
for k, v in sorted(obj.items(), key=lambda x: x[0]):
245+
if k.startswith(prefix):
246+
247+
path = [int_or_string(e) for e in k[len(prefix) :].lower().split("_")]
248+
try:
249+
value = ConfigFactory.parse_string(v)
250+
except pyparsing.ParseSyntaxException:
251+
value = v
211252

212-
if out:
213-
if out.lower() == "hocon":
214-
return HOCONConverter.to_hocon(conf)
215-
if out.lower() == "yaml":
216-
return HOCONConverter.to_yaml(conf)
217-
if out.lower() == "json":
218-
return HOCONConverter.to_json(conf)
219-
if out.lower() == "properties":
220-
return HOCONConverter.to_properties(conf)
253+
set_lens(path, ret, value)
221254

222-
return conf
255+
return ret

dataconf/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import importlib.metadata
22

3-
__version__ = importlib.metadata.version("dataconf")
3+
__version__ = importlib.metadata.version("dataconf").lstrip("v")

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "dataconf"
3-
version = "0.2.1"
3+
version = "0+dev"
44
description = "Lightweight configuration with automatic dataclasses parsing (HOCON/JSON/YAML/PROPERTIES)"
55
authors = []
66
license = "Apache2"

0 commit comments

Comments
 (0)