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 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
- conditions = tuple((k, tuple(v)) for k, v in when.items())
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: dict[str, str], override: Override) -> bool:
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
- default: Mapping[str, Any],
178
- overrides: Sequence[Override],
179
- mapping: dict[str, str],
180
- ) -> dict[str, Any]:
181
- result = copy.deepcopy(default)
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
@@ -20,7 +20,7 @@ class TomlEncodeError(TomlCombineError):
20
20
 
21
21
 
22
22
  class DuplicateError(TomlCombineError):
23
- """In {type} {id}: Cannot have multiple {type}s with the same dimensions."""
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
- default=config_obj.default,
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.4.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 overriden. Overrides are applied in order from less
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
- - If an override contains conditions on more dimensions than another one, it's applied
69
- later
70
- - In case 2 overrides contain the same number of dimensions and they're a disjoint set,
71
- then it depends on how the dimensions are defined at the top of the file: dimensions
72
- defined last have a greater priority
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 define
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, while
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
- [dimensions]
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
- name = "service-dev"
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,,