omlish 0.0.0.dev192__py3-none-any.whl → 0.0.0.dev194__py3-none-any.whl
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.
- omlish/.manifests.json +17 -3
- omlish/__about__.py +2 -2
- omlish/codecs/base.py +2 -0
- omlish/configs/__init__.py +0 -5
- omlish/configs/formats.py +247 -0
- omlish/configs/nginx.py +72 -0
- omlish/configs/processing/__init__.py +0 -0
- omlish/configs/{flattening.py → processing/flattening.py} +31 -33
- omlish/configs/processing/inheritance.py +58 -0
- omlish/configs/processing/matching.py +53 -0
- omlish/configs/processing/names.py +50 -0
- omlish/configs/processing/rewriting.py +141 -0
- omlish/configs/processing/strings.py +45 -0
- omlish/configs/types.py +9 -0
- omlish/formats/__init__.py +4 -0
- omlish/formats/ini/__init__.py +0 -0
- omlish/formats/ini/codec.py +26 -0
- omlish/formats/ini/sections.py +45 -0
- omlish/formats/toml/__init__.py +0 -0
- omlish/formats/{toml.py → toml/codec.py} +2 -2
- omlish/formats/toml/parser.py +827 -0
- omlish/formats/toml/writer.py +124 -0
- omlish/lang/__init__.py +4 -1
- omlish/lang/iterables.py +0 -7
- omlish/lite/configs.py +38 -0
- omlish/lite/types.py +9 -0
- omlish/text/glyphsplit.py +25 -10
- {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/RECORD +33 -17
- omlish/configs/strings.py +0 -96
- {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/top_level.txt +0 -0
omlish/.manifests.json
CHANGED
@@ -51,6 +51,20 @@
|
|
51
51
|
}
|
52
52
|
}
|
53
53
|
},
|
54
|
+
{
|
55
|
+
"module": ".formats.ini.codec",
|
56
|
+
"attr": "_INI_LAZY_CODEC",
|
57
|
+
"file": "omlish/formats/ini/codec.py",
|
58
|
+
"line": 25,
|
59
|
+
"value": {
|
60
|
+
"$.codecs.base.LazyLoadedCodec": {
|
61
|
+
"mod_name": "omlish.formats.ini.codec",
|
62
|
+
"attr_name": "INI_CODEC",
|
63
|
+
"name": "ini",
|
64
|
+
"aliases": null
|
65
|
+
}
|
66
|
+
}
|
67
|
+
},
|
54
68
|
{
|
55
69
|
"module": ".formats.json.codecs",
|
56
70
|
"attr": "_JSON_LAZY_CODEC",
|
@@ -136,13 +150,13 @@
|
|
136
150
|
}
|
137
151
|
},
|
138
152
|
{
|
139
|
-
"module": ".formats.toml",
|
153
|
+
"module": ".formats.toml.codec",
|
140
154
|
"attr": "_TOML_LAZY_CODEC",
|
141
|
-
"file": "omlish/formats/toml.py",
|
155
|
+
"file": "omlish/formats/toml/codec.py",
|
142
156
|
"line": 16,
|
143
157
|
"value": {
|
144
158
|
"$.codecs.base.LazyLoadedCodec": {
|
145
|
-
"mod_name": "omlish.formats.toml",
|
159
|
+
"mod_name": "omlish.formats.toml.codec",
|
146
160
|
"attr_name": "TOML_CODEC",
|
147
161
|
"name": "toml",
|
148
162
|
"aliases": null
|
omlish/__about__.py
CHANGED
omlish/codecs/base.py
CHANGED
omlish/configs/__init__.py
CHANGED
@@ -0,0 +1,247 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
Notes:
|
5
|
+
- necessarily string-oriented
|
6
|
+
- single file, as this is intended to be amalg'd and thus all included anyway
|
7
|
+
|
8
|
+
TODO:
|
9
|
+
- ConfigDataMapper? to_map -> ConfigMap?
|
10
|
+
- nginx ?
|
11
|
+
- raw ?
|
12
|
+
"""
|
13
|
+
import abc
|
14
|
+
import collections.abc
|
15
|
+
import configparser
|
16
|
+
import dataclasses as dc
|
17
|
+
import json
|
18
|
+
import os.path
|
19
|
+
import typing as ta
|
20
|
+
|
21
|
+
from ..formats.ini.sections import IniSectionSettingsMap
|
22
|
+
from ..formats.ini.sections import extract_ini_sections
|
23
|
+
from ..formats.ini.sections import render_ini_sections
|
24
|
+
from ..formats.toml.parser import toml_loads
|
25
|
+
from ..formats.toml.writer import TomlWriter
|
26
|
+
from ..lite.check import check
|
27
|
+
from ..lite.json import json_dumps_pretty
|
28
|
+
from .types import ConfigMap
|
29
|
+
|
30
|
+
|
31
|
+
T = ta.TypeVar('T')
|
32
|
+
ConfigDataT = ta.TypeVar('ConfigDataT', bound='ConfigData')
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
38
|
+
@dc.dataclass(frozen=True)
|
39
|
+
class ConfigData(abc.ABC): # noqa
|
40
|
+
@abc.abstractmethod
|
41
|
+
def as_map(self) -> ConfigMap:
|
42
|
+
raise NotImplementedError
|
43
|
+
|
44
|
+
|
45
|
+
#
|
46
|
+
|
47
|
+
|
48
|
+
class ConfigLoader(abc.ABC, ta.Generic[ConfigDataT]):
|
49
|
+
@property
|
50
|
+
def file_exts(self) -> ta.Sequence[str]:
|
51
|
+
return ()
|
52
|
+
|
53
|
+
def match_file(self, n: str) -> bool:
|
54
|
+
return '.' in n and n.split('.')[-1] in check.not_isinstance(self.file_exts, str)
|
55
|
+
|
56
|
+
#
|
57
|
+
|
58
|
+
def load_file(self, p: str) -> ConfigDataT:
|
59
|
+
with open(p) as f:
|
60
|
+
return self.load_str(f.read())
|
61
|
+
|
62
|
+
@abc.abstractmethod
|
63
|
+
def load_str(self, s: str) -> ConfigDataT:
|
64
|
+
raise NotImplementedError
|
65
|
+
|
66
|
+
|
67
|
+
#
|
68
|
+
|
69
|
+
|
70
|
+
class ConfigRenderer(abc.ABC, ta.Generic[ConfigDataT]):
|
71
|
+
@property
|
72
|
+
@abc.abstractmethod
|
73
|
+
def data_cls(self) -> ta.Type[ConfigDataT]:
|
74
|
+
raise NotImplementedError
|
75
|
+
|
76
|
+
def match_data(self, d: ConfigDataT) -> bool:
|
77
|
+
return isinstance(d, self.data_cls)
|
78
|
+
|
79
|
+
#
|
80
|
+
|
81
|
+
@abc.abstractmethod
|
82
|
+
def render(self, d: ConfigDataT) -> str:
|
83
|
+
raise NotImplementedError
|
84
|
+
|
85
|
+
|
86
|
+
##
|
87
|
+
|
88
|
+
|
89
|
+
@dc.dataclass(frozen=True)
|
90
|
+
class ObjConfigData(ConfigData, abc.ABC):
|
91
|
+
obj: ta.Any
|
92
|
+
|
93
|
+
def as_map(self) -> ConfigMap:
|
94
|
+
return check.isinstance(self.obj, collections.abc.Mapping)
|
95
|
+
|
96
|
+
|
97
|
+
##
|
98
|
+
|
99
|
+
|
100
|
+
@dc.dataclass(frozen=True)
|
101
|
+
class JsonConfigData(ObjConfigData):
|
102
|
+
pass
|
103
|
+
|
104
|
+
|
105
|
+
class JsonConfigLoader(ConfigLoader[JsonConfigData]):
|
106
|
+
file_exts = ('json',)
|
107
|
+
|
108
|
+
def load_str(self, s: str) -> JsonConfigData:
|
109
|
+
return JsonConfigData(json.loads(s))
|
110
|
+
|
111
|
+
|
112
|
+
class JsonConfigRenderer(ConfigRenderer[JsonConfigData]):
|
113
|
+
data_cls = JsonConfigData
|
114
|
+
|
115
|
+
def render(self, d: JsonConfigData) -> str:
|
116
|
+
return json_dumps_pretty(d.obj)
|
117
|
+
|
118
|
+
|
119
|
+
##
|
120
|
+
|
121
|
+
|
122
|
+
@dc.dataclass(frozen=True)
|
123
|
+
class TomlConfigData(ObjConfigData):
|
124
|
+
pass
|
125
|
+
|
126
|
+
|
127
|
+
class TomlConfigLoader(ConfigLoader[TomlConfigData]):
|
128
|
+
file_exts = ('toml',)
|
129
|
+
|
130
|
+
def load_str(self, s: str) -> TomlConfigData:
|
131
|
+
return TomlConfigData(toml_loads(s))
|
132
|
+
|
133
|
+
|
134
|
+
class TomlConfigRenderer(ConfigRenderer[TomlConfigData]):
|
135
|
+
data_cls = TomlConfigData
|
136
|
+
|
137
|
+
def render(self, d: TomlConfigData) -> str:
|
138
|
+
return TomlWriter.write_str(d.obj)
|
139
|
+
|
140
|
+
|
141
|
+
##
|
142
|
+
|
143
|
+
|
144
|
+
@dc.dataclass(frozen=True)
|
145
|
+
class YamlConfigData(ObjConfigData):
|
146
|
+
pass
|
147
|
+
|
148
|
+
|
149
|
+
class YamlConfigLoader(ConfigLoader[YamlConfigData]):
|
150
|
+
file_exts = ('yaml', 'yml')
|
151
|
+
|
152
|
+
def load_str(self, s: str) -> YamlConfigData:
|
153
|
+
return YamlConfigData(__import__('yaml').safe_load(s))
|
154
|
+
|
155
|
+
|
156
|
+
class YamlConfigRenderer(ConfigRenderer[YamlConfigData]):
|
157
|
+
data_cls = YamlConfigData
|
158
|
+
|
159
|
+
def render(self, d: YamlConfigData) -> str:
|
160
|
+
return __import__('yaml').safe_dump(d.obj)
|
161
|
+
|
162
|
+
|
163
|
+
##
|
164
|
+
|
165
|
+
|
166
|
+
@dc.dataclass(frozen=True)
|
167
|
+
class IniConfigData(ConfigData):
|
168
|
+
sections: IniSectionSettingsMap
|
169
|
+
|
170
|
+
def as_map(self) -> ConfigMap:
|
171
|
+
return self.sections
|
172
|
+
|
173
|
+
|
174
|
+
class IniConfigLoader(ConfigLoader[IniConfigData]):
|
175
|
+
file_exts = ('ini',)
|
176
|
+
|
177
|
+
def load_str(self, s: str) -> IniConfigData:
|
178
|
+
cp = configparser.ConfigParser()
|
179
|
+
cp.read_string(s)
|
180
|
+
return IniConfigData(extract_ini_sections(cp))
|
181
|
+
|
182
|
+
|
183
|
+
class IniConfigRenderer(ConfigRenderer[IniConfigData]):
|
184
|
+
data_cls = IniConfigData
|
185
|
+
|
186
|
+
def render(self, d: IniConfigData) -> str:
|
187
|
+
return render_ini_sections(d.sections)
|
188
|
+
|
189
|
+
|
190
|
+
##
|
191
|
+
|
192
|
+
|
193
|
+
@dc.dataclass(frozen=True)
|
194
|
+
class SwitchedConfigFileLoader:
|
195
|
+
loaders: ta.Sequence[ConfigLoader]
|
196
|
+
default: ta.Optional[ConfigLoader] = None
|
197
|
+
|
198
|
+
def load_file(self, p: str) -> ConfigData:
|
199
|
+
n = os.path.basename(p)
|
200
|
+
|
201
|
+
for l in self.loaders:
|
202
|
+
if l.match_file(n):
|
203
|
+
return l.load_file(p)
|
204
|
+
|
205
|
+
if (d := self.default) is not None:
|
206
|
+
return d.load_file(p)
|
207
|
+
|
208
|
+
raise NameError(n)
|
209
|
+
|
210
|
+
|
211
|
+
DEFAULT_CONFIG_LOADERS: ta.Sequence[ConfigLoader] = [
|
212
|
+
JsonConfigLoader(),
|
213
|
+
TomlConfigLoader(),
|
214
|
+
YamlConfigLoader(),
|
215
|
+
IniConfigLoader(),
|
216
|
+
]
|
217
|
+
|
218
|
+
DEFAULT_CONFIG_LOADER: ConfigLoader = JsonConfigLoader()
|
219
|
+
|
220
|
+
DEFAULT_CONFIG_FILE_LOADER = SwitchedConfigFileLoader(
|
221
|
+
loaders=DEFAULT_CONFIG_LOADERS,
|
222
|
+
default=DEFAULT_CONFIG_LOADER,
|
223
|
+
)
|
224
|
+
|
225
|
+
|
226
|
+
##
|
227
|
+
|
228
|
+
|
229
|
+
@dc.dataclass(frozen=True)
|
230
|
+
class SwitchedConfigRenderer:
|
231
|
+
renderers: ta.Sequence[ConfigRenderer]
|
232
|
+
|
233
|
+
def render(self, d: ConfigData) -> str:
|
234
|
+
for r in self.renderers:
|
235
|
+
if r.match_data(d):
|
236
|
+
return r.render(d)
|
237
|
+
raise TypeError(d)
|
238
|
+
|
239
|
+
|
240
|
+
DEFAULT_CONFIG_RENDERERS: ta.Sequence[ConfigRenderer] = [
|
241
|
+
JsonConfigRenderer(),
|
242
|
+
TomlConfigRenderer(),
|
243
|
+
YamlConfigRenderer(),
|
244
|
+
IniConfigRenderer(),
|
245
|
+
]
|
246
|
+
|
247
|
+
DEFAULT_CONFIG_RENDERER = SwitchedConfigRenderer(DEFAULT_CONFIG_RENDERERS)
|
omlish/configs/nginx.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
See:
|
5
|
+
- https://nginx.org/en/docs/dev/development_guide.html
|
6
|
+
- https://nginx.org/en/docs/dev/development_guide.html#config_directives
|
7
|
+
- https://nginx.org/en/docs/example.html
|
8
|
+
"""
|
9
|
+
import collections.abc
|
10
|
+
import dataclasses as dc
|
11
|
+
import typing as ta
|
12
|
+
|
13
|
+
from ..lite.check import check
|
14
|
+
from ..text.indent import IndentWriter
|
15
|
+
|
16
|
+
|
17
|
+
@dc.dataclass()
|
18
|
+
class NginxConfigItems:
|
19
|
+
lst: ta.List['NginxConfigItem']
|
20
|
+
|
21
|
+
@classmethod
|
22
|
+
def of(cls, obj: ta.Any) -> 'NginxConfigItems':
|
23
|
+
if isinstance(obj, NginxConfigItems):
|
24
|
+
return obj
|
25
|
+
return cls([NginxConfigItem.of(e) for e in check.isinstance(obj, list)])
|
26
|
+
|
27
|
+
|
28
|
+
@dc.dataclass()
|
29
|
+
class NginxConfigItem:
|
30
|
+
name: str
|
31
|
+
args: ta.Optional[ta.List[str]] = None
|
32
|
+
block: ta.Optional[NginxConfigItems] = None
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def of(cls, obj: ta.Any) -> 'NginxConfigItem':
|
36
|
+
if isinstance(obj, NginxConfigItem):
|
37
|
+
return obj
|
38
|
+
args = check.isinstance(check.not_isinstance(obj, str), collections.abc.Sequence)
|
39
|
+
name, args = check.isinstance(args[0], str), args[1:]
|
40
|
+
if args and not isinstance(args[-1], str):
|
41
|
+
block, args = NginxConfigItems.of(args[-1]), args[:-1]
|
42
|
+
else:
|
43
|
+
block = None
|
44
|
+
return NginxConfigItem(name, [check.isinstance(e, str) for e in args], block=block)
|
45
|
+
|
46
|
+
|
47
|
+
def render_nginx_config(wr: IndentWriter, obj: ta.Any) -> None:
|
48
|
+
if isinstance(obj, NginxConfigItem):
|
49
|
+
wr.write(obj.name)
|
50
|
+
for e in obj.args or ():
|
51
|
+
wr.write(' ')
|
52
|
+
wr.write(e)
|
53
|
+
if obj.block:
|
54
|
+
wr.write(' {\n')
|
55
|
+
with wr.indent():
|
56
|
+
render_nginx_config(wr, obj.block)
|
57
|
+
wr.write('}\n')
|
58
|
+
else:
|
59
|
+
wr.write(';\n')
|
60
|
+
|
61
|
+
elif isinstance(obj, NginxConfigItems):
|
62
|
+
for e2 in obj.lst:
|
63
|
+
render_nginx_config(wr, e2)
|
64
|
+
|
65
|
+
else:
|
66
|
+
raise TypeError(obj)
|
67
|
+
|
68
|
+
|
69
|
+
def render_nginx_config_str(obj: ta.Any) -> str:
|
70
|
+
iw = IndentWriter()
|
71
|
+
render_nginx_config(iw, obj)
|
72
|
+
return iw.getvalue()
|
File without changes
|
@@ -1,32 +1,33 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
1
3
|
import abc
|
2
|
-
import itertools
|
3
4
|
import typing as ta
|
4
5
|
|
5
|
-
from
|
6
|
-
from .. import lang
|
6
|
+
from ...lite.check import check
|
7
7
|
|
8
8
|
|
9
9
|
K = ta.TypeVar('K')
|
10
10
|
V = ta.TypeVar('V')
|
11
|
-
StrMap = ta.Mapping[str, ta.Any]
|
12
11
|
|
13
12
|
|
14
|
-
|
15
|
-
pass
|
13
|
+
##
|
16
14
|
|
17
15
|
|
18
|
-
class
|
19
|
-
|
16
|
+
class ConfigFlattening:
|
20
17
|
DEFAULT_DELIMITER = '.'
|
21
18
|
DEFAULT_INDEX_OPEN = '('
|
22
19
|
DEFAULT_INDEX_CLOSE = ')'
|
23
20
|
|
21
|
+
class _MISSING:
|
22
|
+
def __new__(cls, *args, **kwargs): # noqa
|
23
|
+
raise TypeError
|
24
|
+
|
24
25
|
def __init__(
|
25
26
|
self,
|
26
27
|
*,
|
27
|
-
delimiter=DEFAULT_DELIMITER,
|
28
|
-
index_open=DEFAULT_INDEX_OPEN,
|
29
|
-
index_close=DEFAULT_INDEX_CLOSE,
|
28
|
+
delimiter: str = DEFAULT_DELIMITER,
|
29
|
+
index_open: str = DEFAULT_INDEX_OPEN,
|
30
|
+
index_close: str = DEFAULT_INDEX_CLOSE,
|
30
31
|
) -> None:
|
31
32
|
super().__init__()
|
32
33
|
|
@@ -34,8 +35,8 @@ class Flattening:
|
|
34
35
|
self._index_open = check.not_empty(index_open)
|
35
36
|
self._index_close = check.not_empty(index_close)
|
36
37
|
|
37
|
-
def flatten(self, unflattened:
|
38
|
-
def rec(prefix:
|
38
|
+
def flatten(self, unflattened: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.Any]:
|
39
|
+
def rec(prefix: ta.List[str], value: ta.Any) -> None:
|
39
40
|
if isinstance(value, dict):
|
40
41
|
for k, v in value.items():
|
41
42
|
rec([*prefix, k], v)
|
@@ -53,8 +54,7 @@ class Flattening:
|
|
53
54
|
rec([], unflattened)
|
54
55
|
return ret
|
55
56
|
|
56
|
-
class UnflattenNode(
|
57
|
-
|
57
|
+
class UnflattenNode(abc.ABC, ta.Generic[K]):
|
58
58
|
@abc.abstractmethod
|
59
59
|
def get(self, key: K) -> ta.Any:
|
60
60
|
raise NotImplementedError
|
@@ -65,7 +65,7 @@ class Flattening:
|
|
65
65
|
|
66
66
|
def setdefault(self, key: K, supplier: ta.Callable[[], V]) -> V:
|
67
67
|
ret = self.get(key)
|
68
|
-
if ret is _MISSING:
|
68
|
+
if ret is ConfigFlattening._MISSING:
|
69
69
|
ret = supplier()
|
70
70
|
self.put(key, ret)
|
71
71
|
return ret
|
@@ -77,50 +77,48 @@ class Flattening:
|
|
77
77
|
@staticmethod
|
78
78
|
def maybe_build(value: ta.Any) -> ta.Any:
|
79
79
|
check.not_none(value)
|
80
|
-
return value.build() if isinstance(value,
|
80
|
+
return value.build() if isinstance(value, ConfigFlattening.UnflattenNode) else value
|
81
81
|
|
82
82
|
class UnflattenDict(UnflattenNode[str]):
|
83
|
-
|
84
83
|
def __init__(self) -> None:
|
85
84
|
super().__init__()
|
86
85
|
|
87
86
|
self._dict: dict[str, ta.Any] = {}
|
88
87
|
|
89
88
|
def get(self, key: str) -> ta.Any:
|
90
|
-
return self._dict.get(key, _MISSING)
|
89
|
+
return self._dict.get(key, ConfigFlattening._MISSING)
|
91
90
|
|
92
91
|
def put(self, key: str, value: ta.Any) -> None:
|
93
92
|
check.arg(key not in self._dict)
|
94
93
|
self._dict[key] = value
|
95
94
|
|
96
95
|
def build(self) -> ta.Any:
|
97
|
-
return {k:
|
96
|
+
return {k: ConfigFlattening.UnflattenNode.maybe_build(v) for k, v in self._dict.items()}
|
98
97
|
|
99
98
|
class UnflattenList(UnflattenNode[int]):
|
100
|
-
|
101
99
|
def __init__(self) -> None:
|
102
100
|
super().__init__()
|
103
101
|
|
104
|
-
self._list:
|
102
|
+
self._list: ta.List[ta.Any] = []
|
105
103
|
|
106
104
|
def get(self, key: int) -> ta.Any:
|
107
105
|
check.arg(key >= 0)
|
108
|
-
return self._list[key] if key < len(self._list) else _MISSING
|
106
|
+
return self._list[key] if key < len(self._list) else ConfigFlattening._MISSING
|
109
107
|
|
110
108
|
def put(self, key: int, value: ta.Any) -> None:
|
111
109
|
check.arg(key >= 0)
|
112
110
|
if key >= len(self._list):
|
113
|
-
self._list.extend([_MISSING] * (key - len(self._list) + 1))
|
114
|
-
check.arg(self._list[key] is _MISSING)
|
111
|
+
self._list.extend([ConfigFlattening._MISSING] * (key - len(self._list) + 1))
|
112
|
+
check.arg(self._list[key] is ConfigFlattening._MISSING)
|
115
113
|
self._list[key] = value
|
116
114
|
|
117
115
|
def build(self) -> ta.Any:
|
118
|
-
return [
|
116
|
+
return [ConfigFlattening.UnflattenNode.maybe_build(e) for e in self._list]
|
119
117
|
|
120
|
-
def unflatten(self, flattened:
|
121
|
-
root =
|
118
|
+
def unflatten(self, flattened: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.Any]:
|
119
|
+
root = ConfigFlattening.UnflattenDict()
|
122
120
|
|
123
|
-
def split_keys(fkey: str) -> ta.Iterable[str
|
121
|
+
def split_keys(fkey: str) -> ta.Iterable[ta.Union[str, int]]:
|
124
122
|
for part in fkey.split(self._delimiter):
|
125
123
|
if self._index_open in part:
|
126
124
|
check.state(part.endswith(self._index_close))
|
@@ -134,13 +132,13 @@ class Flattening:
|
|
134
132
|
yield part
|
135
133
|
|
136
134
|
for fk, v in flattened.items():
|
137
|
-
node:
|
135
|
+
node: ConfigFlattening.UnflattenNode = root
|
138
136
|
fks = list(split_keys(fk))
|
139
|
-
for key, nkey in
|
137
|
+
for key, nkey in zip(fks, fks[1:]): # noqa
|
140
138
|
if isinstance(nkey, str):
|
141
|
-
node = node.setdefault(key,
|
139
|
+
node = node.setdefault(key, ConfigFlattening.UnflattenDict)
|
142
140
|
elif isinstance(nkey, int):
|
143
|
-
node = node.setdefault(key,
|
141
|
+
node = node.setdefault(key, ConfigFlattening.UnflattenList)
|
144
142
|
else:
|
145
143
|
raise TypeError(key)
|
146
144
|
node.put(fks[-1], v)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
TODO:
|
5
|
+
- custom merging by key - replace, append, setdefault, equality enforcement, ...
|
6
|
+
- better handling of 'None' as signal to replace or not
|
7
|
+
- remove inherited
|
8
|
+
"""
|
9
|
+
import typing as ta
|
10
|
+
|
11
|
+
from ..types import ConfigMap
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
def build_config_inherited_values(
|
18
|
+
m: ta.Mapping[str, ConfigMap],
|
19
|
+
*,
|
20
|
+
inherits_key: str = 'inherits',
|
21
|
+
) -> ta.Dict[str, ConfigMap]:
|
22
|
+
done: ta.Dict[str, ConfigMap] = {}
|
23
|
+
|
24
|
+
def rec(mk: str) -> ta.Mapping[str, ConfigMap]:
|
25
|
+
try:
|
26
|
+
return done[mk]
|
27
|
+
except KeyError:
|
28
|
+
pass
|
29
|
+
|
30
|
+
cur = m[mk]
|
31
|
+
try:
|
32
|
+
inh_seq = cur[inherits_key]
|
33
|
+
except KeyError:
|
34
|
+
done[mk] = cur
|
35
|
+
return cur
|
36
|
+
|
37
|
+
new = dict(cur)
|
38
|
+
for inh in inh_seq:
|
39
|
+
inh_dct = rec(inh)
|
40
|
+
new.update({
|
41
|
+
k: v
|
42
|
+
for k, v in inh_dct.items()
|
43
|
+
if v is not None
|
44
|
+
and cur.get(k) is None
|
45
|
+
})
|
46
|
+
|
47
|
+
out = {
|
48
|
+
k: v
|
49
|
+
for k, v in new.items()
|
50
|
+
if k != inherits_key
|
51
|
+
}
|
52
|
+
|
53
|
+
done[mk] = out
|
54
|
+
return out
|
55
|
+
|
56
|
+
for mk in m:
|
57
|
+
rec(mk)
|
58
|
+
return done
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import dataclasses as dc
|
4
|
+
import functools
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from ...lite.check import check
|
8
|
+
from .rewriting import ConfigRewriter
|
9
|
+
from .rewriting import ConfigRewriterPath
|
10
|
+
|
11
|
+
|
12
|
+
T = ta.TypeVar('T')
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
18
|
+
class MatchingConfigRewriter(ConfigRewriter):
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
fn: ta.Callable[[ta.Any], ta.Any],
|
22
|
+
paths: ta.Iterable[ConfigRewriterPath],
|
23
|
+
*,
|
24
|
+
recurse: bool = False,
|
25
|
+
) -> None:
|
26
|
+
super().__init__()
|
27
|
+
|
28
|
+
self._fn = fn
|
29
|
+
self._paths = frozenset(check.isinstance(p, tuple) for p in paths)
|
30
|
+
self._recurse = recurse
|
31
|
+
|
32
|
+
def rewrite(self, ctx: ConfigRewriter.Context[T]) -> T:
|
33
|
+
if ctx.path in self._paths:
|
34
|
+
no = self._fn(ctx.obj)
|
35
|
+
if not self._recurse:
|
36
|
+
return no
|
37
|
+
ctx = dc.replace(ctx, obj=no)
|
38
|
+
|
39
|
+
return super().rewrite(ctx)
|
40
|
+
|
41
|
+
|
42
|
+
def matched_config_rewrite(
|
43
|
+
fn: ta.Callable[[ta.Any], ta.Any],
|
44
|
+
obj: T,
|
45
|
+
*paths: ConfigRewriterPath,
|
46
|
+
recurse: bool = False,
|
47
|
+
**kwargs: ta.Any,
|
48
|
+
) -> T:
|
49
|
+
return MatchingConfigRewriter(
|
50
|
+
functools.partial(fn, **kwargs),
|
51
|
+
paths,
|
52
|
+
recurse=recurse,
|
53
|
+
)(obj)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
usecase: supervisor process groups
|
5
|
+
"""
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from ...lite.check import check
|
9
|
+
from ..types import ConfigMap
|
10
|
+
|
11
|
+
|
12
|
+
##
|
13
|
+
|
14
|
+
|
15
|
+
def build_config_named_children(
|
16
|
+
o: ta.Union[
|
17
|
+
ta.Sequence[ConfigMap],
|
18
|
+
ta.Mapping[str, ConfigMap],
|
19
|
+
None,
|
20
|
+
],
|
21
|
+
*,
|
22
|
+
name_key: str = 'name',
|
23
|
+
) -> ta.Optional[ta.Sequence[ConfigMap]]:
|
24
|
+
if o is None:
|
25
|
+
return None
|
26
|
+
|
27
|
+
lst: ta.List[ConfigMap] = []
|
28
|
+
if isinstance(o, ta.Mapping):
|
29
|
+
for k, v in o.items():
|
30
|
+
check.isinstance(v, ta.Mapping)
|
31
|
+
if name_key in v:
|
32
|
+
n = v[name_key]
|
33
|
+
if k != n:
|
34
|
+
raise KeyError(f'Given names do not match: {n} != {k}')
|
35
|
+
lst.append(v)
|
36
|
+
else:
|
37
|
+
lst.append({name_key: k, **v})
|
38
|
+
|
39
|
+
else:
|
40
|
+
check.not_isinstance(o, str)
|
41
|
+
lst.extend(o)
|
42
|
+
|
43
|
+
seen = set()
|
44
|
+
for d in lst:
|
45
|
+
n = d['name']
|
46
|
+
if n in d:
|
47
|
+
raise KeyError(f'Duplicate name: {n}')
|
48
|
+
seen.add(n)
|
49
|
+
|
50
|
+
return lst
|