toml-combine 0.4.0__py3-none-any.whl → 0.6.0__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.
- toml_combine/combiner.py +61 -57
- toml_combine/exceptions.py +1 -1
- toml_combine/lib.py +1 -2
- {toml_combine-0.4.0.dist-info → toml_combine-0.6.0.dist-info}/METADATA +37 -20
- toml_combine-0.6.0.dist-info/RECORD +11 -0
- toml_combine-0.4.0.dist-info/RECORD +0 -11
- {toml_combine-0.4.0.dist-info → toml_combine-0.6.0.dist-info}/WHEEL +0 -0
- {toml_combine-0.4.0.dist-info → toml_combine-0.6.0.dist-info}/entry_points.txt +0 -0
toml_combine/combiner.py
CHANGED
@@ -2,8 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import copy
|
4
4
|
import dataclasses
|
5
|
-
from collections.abc import Mapping, Sequence
|
6
|
-
from functools import partial
|
5
|
+
from collections.abc import Iterable, Mapping, Sequence
|
7
6
|
from typing import Any, TypeVar
|
8
7
|
|
9
8
|
from . import exceptions
|
@@ -59,36 +58,6 @@ def clean_dimensions_dict(
|
|
59
58
|
return result
|
60
59
|
|
61
60
|
|
62
|
-
def override_sort_key(
|
63
|
-
override: Override, dimensions: dict[str, list[str]]
|
64
|
-
) -> tuple[int, ...]:
|
65
|
-
"""
|
66
|
-
We sort overrides before applying them, and they are applied in the order of the
|
67
|
-
sorted list, each override replacing the common values of the previous overrides.
|
68
|
-
|
69
|
-
override_sort_key defines the sort key for overrides that ensures less specific
|
70
|
-
overrides come first:
|
71
|
-
- Overrides with fewer dimensions come first (will be overridden
|
72
|
-
by more specific ones)
|
73
|
-
- If two overrides have the same number of dimensions but define different
|
74
|
-
dimensions, we sort by the definition order of the dimensions.
|
75
|
-
|
76
|
-
Example:
|
77
|
-
dimensions = {"env": ["dev", "prod"], "region": ["us", "eu"]}
|
78
|
-
|
79
|
-
- Override with {"env": "dev"} comes before override with
|
80
|
-
{"env": "dev", "region": "us"} (less specific)
|
81
|
-
- Override with {"env": "dev"} comes before override with {"region": "us"} ("env"
|
82
|
-
is defined before "region" in the dimensions list)
|
83
|
-
"""
|
84
|
-
result = [len(override.when)]
|
85
|
-
for i, dimension in enumerate(dimensions):
|
86
|
-
if dimension in override.when:
|
87
|
-
result.append(i)
|
88
|
-
|
89
|
-
return tuple(result)
|
90
|
-
|
91
|
-
|
92
61
|
T = TypeVar("T", dict, list, str, int, float, bool)
|
93
62
|
|
94
63
|
|
@@ -111,6 +80,41 @@ def merge_configs(a: T, b: T, /) -> T:
|
|
111
80
|
return result
|
112
81
|
|
113
82
|
|
83
|
+
def extract_keys(config: Any) -> Iterable[tuple[str, ...]]:
|
84
|
+
"""
|
85
|
+
Extract the keys from a config.
|
86
|
+
"""
|
87
|
+
if isinstance(config, dict):
|
88
|
+
for key, value in config.items():
|
89
|
+
for sub_key in extract_keys(value):
|
90
|
+
yield (key, *sub_key)
|
91
|
+
else:
|
92
|
+
yield tuple()
|
93
|
+
|
94
|
+
|
95
|
+
def are_conditions_compatible(
|
96
|
+
a: Mapping[str, list[str]], b: Mapping[str, list[str]], /
|
97
|
+
) -> bool:
|
98
|
+
"""
|
99
|
+
`a` and `b` are dictionaries representing override conditions (`when`). Return
|
100
|
+
`True` if the conditions represented by `a` are compatible. Conditions are
|
101
|
+
compatible if one is stricly more specific than the other or if they're mutually
|
102
|
+
exclusive.
|
103
|
+
"""
|
104
|
+
# Subset
|
105
|
+
if set(a) < set(b) or set(b) < set(a):
|
106
|
+
return True
|
107
|
+
|
108
|
+
# Disjoint or overlapping sets
|
109
|
+
if set(a) != set(b):
|
110
|
+
return False
|
111
|
+
|
112
|
+
# Equal sets: it's only compatible if the values are disjoint
|
113
|
+
if any(set(a[key]) & set(b[key]) for key in a.keys()):
|
114
|
+
return False
|
115
|
+
return True
|
116
|
+
|
117
|
+
|
114
118
|
def build_config(config: dict[str, Any]) -> Config:
|
115
119
|
config = copy.deepcopy(config)
|
116
120
|
# Parse dimensions
|
@@ -119,7 +123,6 @@ def build_config(config: dict[str, Any]) -> Config:
|
|
119
123
|
# Parse template
|
120
124
|
default = config.pop("default", {})
|
121
125
|
|
122
|
-
seen_conditions = set()
|
123
126
|
overrides = []
|
124
127
|
for override in config.pop("override", []):
|
125
128
|
try:
|
@@ -132,25 +135,10 @@ def build_config(config: dict[str, Any]) -> Config:
|
|
132
135
|
type="override",
|
133
136
|
)
|
134
137
|
|
135
|
-
|
136
|
-
if conditions in seen_conditions:
|
137
|
-
raise exceptions.DuplicateError(type="override", id=when)
|
138
|
-
|
139
|
-
seen_conditions.add(conditions)
|
138
|
+
overrides.append(Override(when=when, config=override))
|
140
139
|
|
141
|
-
overrides.append(
|
142
|
-
Override(
|
143
|
-
when=clean_dimensions_dict(
|
144
|
-
to_sort=when, clean=dimensions, type="override"
|
145
|
-
),
|
146
|
-
config=override,
|
147
|
-
)
|
148
|
-
)
|
149
140
|
# Sort overrides by increasing specificity
|
150
|
-
overrides = sorted(
|
151
|
-
overrides,
|
152
|
-
key=partial(override_sort_key, dimensions=dimensions),
|
153
|
-
)
|
141
|
+
overrides = sorted(overrides, key=lambda override: len(override.when))
|
154
142
|
|
155
143
|
return Config(
|
156
144
|
dimensions=dimensions,
|
@@ -159,7 +147,7 @@ def build_config(config: dict[str, Any]) -> Config:
|
|
159
147
|
)
|
160
148
|
|
161
149
|
|
162
|
-
def mapping_matches_override(mapping:
|
150
|
+
def mapping_matches_override(mapping: Mapping[str, str], override: Override) -> bool:
|
163
151
|
"""
|
164
152
|
Check if the values in the override match the given dimensions.
|
165
153
|
"""
|
@@ -174,16 +162,32 @@ def mapping_matches_override(mapping: dict[str, str], override: Override) -> boo
|
|
174
162
|
|
175
163
|
|
176
164
|
def generate_for_mapping(
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
165
|
+
config: Config,
|
166
|
+
mapping: Mapping[str, str],
|
167
|
+
) -> Mapping[str, Any]:
|
168
|
+
result = copy.deepcopy(config.default)
|
169
|
+
keys_to_conditions: dict[tuple[str, ...], list[dict[str, list[str]]]] = {}
|
182
170
|
# Apply each matching override
|
183
|
-
for override in overrides:
|
171
|
+
for override in config.overrides:
|
184
172
|
# Check if all dimension values in the override match
|
185
173
|
|
186
174
|
if mapping_matches_override(mapping=mapping, override=override):
|
175
|
+
# Check that all applicableoverrides are compatible
|
176
|
+
keys = extract_keys(override.config)
|
177
|
+
|
178
|
+
for key in keys:
|
179
|
+
previous_conditions = keys_to_conditions.setdefault(key, [])
|
180
|
+
|
181
|
+
for previous_condition in previous_conditions:
|
182
|
+
if not are_conditions_compatible(previous_condition, override.when):
|
183
|
+
raise exceptions.DuplicateError(
|
184
|
+
id=override.when,
|
185
|
+
key=".".join(key),
|
186
|
+
other_override=previous_condition,
|
187
|
+
)
|
188
|
+
|
189
|
+
keys_to_conditions[key].append(override.when)
|
190
|
+
|
187
191
|
result = merge_configs(result, override.config)
|
188
192
|
|
189
193
|
return result
|
toml_combine/exceptions.py
CHANGED
@@ -20,7 +20,7 @@ class TomlEncodeError(TomlCombineError):
|
|
20
20
|
|
21
21
|
|
22
22
|
class DuplicateError(TomlCombineError):
|
23
|
-
"""In
|
23
|
+
"""In override {id}: Overrides defining the same configuration keys must be included in one another or mutually exclusive.\nKey defined multiple times: {key}\nOther override: {other_override}"""
|
24
24
|
|
25
25
|
|
26
26
|
class DimensionNotFound(TomlCombineError):
|
toml_combine/lib.py
CHANGED
@@ -52,7 +52,6 @@ def combine(*, config=None, config_file=None, **mapping):
|
|
52
52
|
config_obj = combiner.build_config(dict_config)
|
53
53
|
|
54
54
|
return combiner.generate_for_mapping(
|
55
|
-
|
56
|
-
overrides=config_obj.overrides,
|
55
|
+
config=config_obj,
|
57
56
|
mapping=mapping,
|
58
57
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: toml-combine
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Summary: A tool for combining complex configurations in TOML format.
|
5
5
|
Author-email: Joachim Jablon <ewjoachim@gmail.com>
|
6
6
|
License-Expression: MIT
|
@@ -29,7 +29,7 @@ parts that are common to everyone.
|
|
29
29
|
|
30
30
|
### The config file
|
31
31
|
|
32
|
-
The configuration file is usually a TOML file. Here's a small example:
|
32
|
+
The configuration file is (usually) a TOML file. Here's a small example:
|
33
33
|
|
34
34
|
```toml
|
35
35
|
[dimensions]
|
@@ -62,14 +62,19 @@ The common configuration to start from, before we start overlaying overrides on
|
|
62
62
|
### Overrides
|
63
63
|
|
64
64
|
Overrides define a set of condition where they apply (`when.<dimension> =
|
65
|
-
"<value>"`) and the values that are
|
66
|
-
specific to more specific, each one overriding the values of the previous ones:
|
65
|
+
"<value>"`) and the values that are overridden when they're applicable.
|
67
66
|
|
68
|
-
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
67
|
+
- In case 2 overrides are applicable and define a value for the same key, if one is more
|
68
|
+
specific than the other (e.g. env=prod,region=us is more specific than env=prod) then
|
69
|
+
its values will have precedence.
|
70
|
+
- If they are mutually exclusive, (env=prod vs env=staging) precedence is irrelevant.
|
71
|
+
- If you try to generate the configuration and 2 applicable overrides define a value for
|
72
|
+
the same key, an error will be raised (e.g. env=staging and region=eu). In that case,
|
73
|
+
you should add dimensions to either override to make them mutually exclusive or make
|
74
|
+
one more specific than the other.
|
75
|
+
|
76
|
+
Note that it's not a problem if incompatible overrides exist in your configuration, as
|
77
|
+
long as they are not both applicable in the same call.
|
73
78
|
|
74
79
|
> [!Note]
|
75
80
|
> Defining a list as the value of one or more conditions in an override
|
@@ -77,14 +82,11 @@ specific to more specific, each one overriding the values of the previous ones:
|
|
77
82
|
|
78
83
|
### The configuration itself
|
79
84
|
|
80
|
-
Under the layer of `dimensions/default/override/mapping` system, what you actually
|
81
|
-
in the configuration is completely up to you. That said, only nested
|
85
|
+
Under the layer of `dimensions/default/override/mapping` system, what you actually
|
86
|
+
define in the configuration is completely up to you. That said, only nested
|
82
87
|
"dictionnaries"/"objects"/"tables"/"mapping" (those are all the same things in
|
83
|
-
Python/JS/Toml lingo) will be merged between the default and the overrides,
|
84
|
-
arrays will just replace one another. See `Arrays` below.
|
85
|
-
|
86
|
-
In the generated configuration, the dimensions of the output will appear in the generated
|
87
|
-
object as an object under the `dimensions` key.
|
88
|
+
Python/JS/Toml lingo) will be merged between the default and the applicable overrides,
|
89
|
+
while arrays will just replace one another. See `Arrays` below.
|
88
90
|
|
89
91
|
### Arrays
|
90
92
|
|
@@ -141,9 +143,9 @@ Example with the config from the previous section:
|
|
141
143
|
|
142
144
|
```console
|
143
145
|
$ toml-combine path/to/config.toml --environment=staging
|
144
|
-
|
145
|
-
environment = "staging"
|
146
|
+
```
|
146
147
|
|
148
|
+
```toml
|
147
149
|
[fruits]
|
148
150
|
apple.color = "red"
|
149
151
|
orange.color = "orange"
|
@@ -188,8 +190,9 @@ container.image_name = "my-image-backend"
|
|
188
190
|
container.port = 8080
|
189
191
|
|
190
192
|
[[override]]
|
191
|
-
|
193
|
+
when.service = "backend"
|
192
194
|
when.environment = "dev"
|
195
|
+
name = "service-dev"
|
193
196
|
container.env.DEBUG = true
|
194
197
|
|
195
198
|
[[override]]
|
@@ -202,6 +205,9 @@ This produces the following configs:
|
|
202
205
|
|
203
206
|
```console
|
204
207
|
$ toml-combine example.toml --environment=production --service=frontend
|
208
|
+
```
|
209
|
+
|
210
|
+
```toml
|
205
211
|
registry = "gcr.io/my-project/"
|
206
212
|
service_account = "my-service-account"
|
207
213
|
name = "service-frontend"
|
@@ -212,6 +218,9 @@ image_name = "my-image-frontend"
|
|
212
218
|
|
213
219
|
```console
|
214
220
|
$ toml-combine example.toml --environment=production --service=backend
|
221
|
+
```
|
222
|
+
|
223
|
+
```toml
|
215
224
|
registry = "gcr.io/my-project/"
|
216
225
|
service_account = "my-service-account"
|
217
226
|
name = "service-backend"
|
@@ -223,6 +232,9 @@ port = 8080
|
|
223
232
|
|
224
233
|
```console
|
225
234
|
$ toml-combine example.toml --environment=staging --service=frontend
|
235
|
+
```
|
236
|
+
|
237
|
+
```toml
|
226
238
|
registry = "gcr.io/my-project/"
|
227
239
|
service_account = "my-service-account"
|
228
240
|
name = "service-frontend"
|
@@ -233,6 +245,9 @@ image_name = "my-image-frontend"
|
|
233
245
|
|
234
246
|
```console
|
235
247
|
$ toml-combine example.toml --environment=staging --service=backend
|
248
|
+
```
|
249
|
+
|
250
|
+
```toml
|
236
251
|
registry = "gcr.io/my-project/"
|
237
252
|
service_account = "my-service-account"
|
238
253
|
name = "service-backend"
|
@@ -247,6 +262,9 @@ ENABLE_EXPENSIVE_MONITORING = false
|
|
247
262
|
|
248
263
|
```console
|
249
264
|
$ toml-combine example.toml --environment=dev --service=backend
|
265
|
+
```
|
266
|
+
|
267
|
+
```toml
|
250
268
|
registry = "gcr.io/my-project/"
|
251
269
|
service_account = "my-service-account"
|
252
270
|
name = "service-backend"
|
@@ -257,5 +275,4 @@ port = 8080
|
|
257
275
|
[container.env]
|
258
276
|
DEBUG = true
|
259
277
|
ENABLE_EXPENSIVE_MONITORING = false
|
260
|
-
|
261
278
|
```
|
@@ -0,0 +1,11 @@
|
|
1
|
+
toml_combine/__init__.py,sha256=TDkOwwEM-nS6hOh79u9Qae6g2Q6VfANpPpnKGfSgu80,84
|
2
|
+
toml_combine/__main__.py,sha256=hmF8N8xX6UEApzbKTVZ-4E1HU5-rjgUkdXNLO-mF6vo,100
|
3
|
+
toml_combine/cli.py,sha256=hG03eDKz7xU-ydJIa1kDuu6WlFzNS3GTMJ6zals9M9c,2843
|
4
|
+
toml_combine/combiner.py,sha256=nwq3q06UhXZkM2Nch_gEnn7K8WUlKbbYxFk1myH63CE,5649
|
5
|
+
toml_combine/exceptions.py,sha256=2b-EkmSoe6bbuE7txDVjEDuix_9bfLQrapkNhy8i-lU,1109
|
6
|
+
toml_combine/lib.py,sha256=jh6OG57JefpGa-WE-mLSIK6KjyJ0-1yGBynr_kiVTww,1634
|
7
|
+
toml_combine/toml.py,sha256=iBV8xj0qWcvGp2AZaML8FCT3i2X9DL7iA6jd-wcP5Bc,814
|
8
|
+
toml_combine-0.6.0.dist-info/METADATA,sha256=1TbVtui8W4B4zumo-rCpPnnuxx83GD5Xkyk-rRqCE5E,7625
|
9
|
+
toml_combine-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
toml_combine-0.6.0.dist-info/entry_points.txt,sha256=dXUQNom54uZt_7ylEG81iNYMamYpaFo9-ItcZJU6Uzc,58
|
11
|
+
toml_combine-0.6.0.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
toml_combine/__init__.py,sha256=TDkOwwEM-nS6hOh79u9Qae6g2Q6VfANpPpnKGfSgu80,84
|
2
|
-
toml_combine/__main__.py,sha256=hmF8N8xX6UEApzbKTVZ-4E1HU5-rjgUkdXNLO-mF6vo,100
|
3
|
-
toml_combine/cli.py,sha256=hG03eDKz7xU-ydJIa1kDuu6WlFzNS3GTMJ6zals9M9c,2843
|
4
|
-
toml_combine/combiner.py,sha256=RhhCevncnVvxFYNywvtVWkVMpiqtF0mq_APjg76Tg4Q,5546
|
5
|
-
toml_combine/exceptions.py,sha256=tAFTDRSg6d10bBruBhsasZXrNNgLTmr_nKfvIsRR_yU,991
|
6
|
-
toml_combine/lib.py,sha256=Iw7F8SCyQMlhaqSD2vtnmM6jbnrgzCZeX0d-LTM3VVg,1683
|
7
|
-
toml_combine/toml.py,sha256=iBV8xj0qWcvGp2AZaML8FCT3i2X9DL7iA6jd-wcP5Bc,814
|
8
|
-
toml_combine-0.4.0.dist-info/METADATA,sha256=WKLq2pGpPRwfLhdpBuqxoAtCwVKXipgcega_4GTl_r4,7347
|
9
|
-
toml_combine-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
-
toml_combine-0.4.0.dist-info/entry_points.txt,sha256=dXUQNom54uZt_7ylEG81iNYMamYpaFo9-ItcZJU6Uzc,58
|
11
|
-
toml_combine-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|