toml-combine 0.1.7__tar.gz → 0.1.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: toml-combine
3
- Version: 0.1.7
3
+ Version: 0.1.9
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
@@ -146,6 +146,41 @@ This example is simple because `name` is a natural choice for the key. In some c
146
146
  the choice is less natural, but you can always decide to name the elements of your
147
147
  list and use that name as a key. Also, yes, you'll loose ordering.
148
148
 
149
+ ### CLI
150
+
151
+ ```console
152
+ $ toml-combine {path/to/config.toml}
153
+ ```
154
+
155
+ Generates all the outputs described by the given TOML config.
156
+
157
+ Note that you can restrict generation to some dimension values by passing
158
+ `--{dimension}={value}`
159
+
160
+ ## Lib
161
+
162
+ ```python
163
+ import toml_combine
164
+
165
+
166
+ result = toml_combine.combine(
167
+ config_file=config_file,
168
+ environment=["production", "staging"],
169
+ type="job",
170
+ job=["manage", "special-command"],
171
+ )
172
+
173
+ print(result)
174
+ {
175
+ "production-job-manage": {...},
176
+ "production-job-special-command": {...},
177
+ "staging-job-manage": {...},
178
+ "staging-job-special-command": {...},
179
+ }
180
+ ```
181
+
182
+ You can pass either `config` (TOML string or dict) or `config_file` (`pathlib.Path` or string path) to `combine()`. Additional `kwargs` restrict the output.
183
+
149
184
  ### A bigger example
150
185
 
151
186
  ```toml
@@ -181,39 +216,90 @@ container.port = 8080
181
216
  name = "service-dev"
182
217
  when.environment = "dev"
183
218
  container.env.DEBUG = true
184
- ```
185
-
186
- ### CLI
187
219
 
188
- ```console
189
- $ toml-combine {path/to/config.toml}
220
+ [[override]]
221
+ when.environment = ["staging", "dev"]
222
+ when.service = "backend"
223
+ container.env.ENABLE_EXPENSIVE_MONITORING = false
190
224
  ```
191
225
 
192
- Generates all the outputs described by the given TOML config.
193
-
194
- Note that you can restrict generation to some dimension values by passing
195
- `--{dimension}={value}`
226
+ This produces the following configs:
196
227
 
197
- ## Lib
198
-
199
- ```python
200
- import toml_combine
201
-
202
-
203
- result = toml_combine.combine(
204
- config_file=config_file,
205
- environment=["production", "staging"],
206
- type="job",
207
- job=["manage", "special-command"],
208
- )
209
-
210
- print(result)
228
+ ```json
211
229
  {
212
- "production-job-manage": {...},
213
- "production-job-special-command": {...},
214
- "staging-job-manage": {...},
215
- "staging-job-special-command": {...},
230
+ "production-frontend-eu": {
231
+ "dimensions": {
232
+ "environment": "production",
233
+ "service": "frontend",
234
+ "region": "eu"
235
+ },
236
+ "registry": "gcr.io/my-project/",
237
+ "service_account": "my-service-account",
238
+ "name": "service-frontend",
239
+ "container": {
240
+ "image_name": "my-image-frontend"
241
+ }
242
+ },
243
+ "production-backend-eu": {
244
+ "dimensions": {
245
+ "environment": "production",
246
+ "service": "backend",
247
+ "region": "eu"
248
+ },
249
+ "registry": "gcr.io/my-project/",
250
+ "service_account": "my-service-account",
251
+ "name": "service-backend",
252
+ "container": {
253
+ "image_name": "my-image-backend",
254
+ "port": 8080
255
+ }
256
+ },
257
+ "staging-frontend-eu": {
258
+ "dimensions": {
259
+ "environment": "staging",
260
+ "service": "frontend",
261
+ "region": "eu"
262
+ },
263
+ "registry": "gcr.io/my-project/",
264
+ "service_account": "my-service-account",
265
+ "name": "service-frontend",
266
+ "container": {
267
+ "image_name": "my-image-frontend"
268
+ }
269
+ },
270
+ "staging-backend-eu": {
271
+ "dimensions": {
272
+ "environment": "staging",
273
+ "service": "backend",
274
+ "region": "eu"
275
+ },
276
+ "registry": "gcr.io/my-project/",
277
+ "service_account": "my-service-account",
278
+ "name": "service-backend",
279
+ "container": {
280
+ "image_name": "my-image-backend",
281
+ "port": 8080,
282
+ "env": {
283
+ "ENABLE_EXPENSIVE_MONITORING": false
284
+ }
285
+ }
286
+ },
287
+ "dev-backend": {
288
+ "dimensions": {
289
+ "environment": "dev",
290
+ "service": "backend"
291
+ },
292
+ "registry": "gcr.io/my-project/",
293
+ "service_account": "my-service-account",
294
+ "name": "service-backend",
295
+ "container": {
296
+ "env": {
297
+ "DEBUG": true,
298
+ "ENABLE_EXPENSIVE_MONITORING": false
299
+ },
300
+ "image_name": "my-image-backend",
301
+ "port": 8080
302
+ }
303
+ }
216
304
  }
217
305
  ```
218
-
219
- You can pass either `config` (TOML string or dict) or `config_file` (`pathlib.Path` or string path) to `combine()`. Additional `kwargs` restrict the output.
@@ -127,6 +127,41 @@ This example is simple because `name` is a natural choice for the key. In some c
127
127
  the choice is less natural, but you can always decide to name the elements of your
128
128
  list and use that name as a key. Also, yes, you'll loose ordering.
129
129
 
130
+ ### CLI
131
+
132
+ ```console
133
+ $ toml-combine {path/to/config.toml}
134
+ ```
135
+
136
+ Generates all the outputs described by the given TOML config.
137
+
138
+ Note that you can restrict generation to some dimension values by passing
139
+ `--{dimension}={value}`
140
+
141
+ ## Lib
142
+
143
+ ```python
144
+ import toml_combine
145
+
146
+
147
+ result = toml_combine.combine(
148
+ config_file=config_file,
149
+ environment=["production", "staging"],
150
+ type="job",
151
+ job=["manage", "special-command"],
152
+ )
153
+
154
+ print(result)
155
+ {
156
+ "production-job-manage": {...},
157
+ "production-job-special-command": {...},
158
+ "staging-job-manage": {...},
159
+ "staging-job-special-command": {...},
160
+ }
161
+ ```
162
+
163
+ You can pass either `config` (TOML string or dict) or `config_file` (`pathlib.Path` or string path) to `combine()`. Additional `kwargs` restrict the output.
164
+
130
165
  ### A bigger example
131
166
 
132
167
  ```toml
@@ -162,39 +197,90 @@ container.port = 8080
162
197
  name = "service-dev"
163
198
  when.environment = "dev"
164
199
  container.env.DEBUG = true
165
- ```
166
-
167
- ### CLI
168
200
 
169
- ```console
170
- $ toml-combine {path/to/config.toml}
201
+ [[override]]
202
+ when.environment = ["staging", "dev"]
203
+ when.service = "backend"
204
+ container.env.ENABLE_EXPENSIVE_MONITORING = false
171
205
  ```
172
206
 
173
- Generates all the outputs described by the given TOML config.
174
-
175
- Note that you can restrict generation to some dimension values by passing
176
- `--{dimension}={value}`
207
+ This produces the following configs:
177
208
 
178
- ## Lib
179
-
180
- ```python
181
- import toml_combine
182
-
183
-
184
- result = toml_combine.combine(
185
- config_file=config_file,
186
- environment=["production", "staging"],
187
- type="job",
188
- job=["manage", "special-command"],
189
- )
190
-
191
- print(result)
209
+ ```json
192
210
  {
193
- "production-job-manage": {...},
194
- "production-job-special-command": {...},
195
- "staging-job-manage": {...},
196
- "staging-job-special-command": {...},
211
+ "production-frontend-eu": {
212
+ "dimensions": {
213
+ "environment": "production",
214
+ "service": "frontend",
215
+ "region": "eu"
216
+ },
217
+ "registry": "gcr.io/my-project/",
218
+ "service_account": "my-service-account",
219
+ "name": "service-frontend",
220
+ "container": {
221
+ "image_name": "my-image-frontend"
222
+ }
223
+ },
224
+ "production-backend-eu": {
225
+ "dimensions": {
226
+ "environment": "production",
227
+ "service": "backend",
228
+ "region": "eu"
229
+ },
230
+ "registry": "gcr.io/my-project/",
231
+ "service_account": "my-service-account",
232
+ "name": "service-backend",
233
+ "container": {
234
+ "image_name": "my-image-backend",
235
+ "port": 8080
236
+ }
237
+ },
238
+ "staging-frontend-eu": {
239
+ "dimensions": {
240
+ "environment": "staging",
241
+ "service": "frontend",
242
+ "region": "eu"
243
+ },
244
+ "registry": "gcr.io/my-project/",
245
+ "service_account": "my-service-account",
246
+ "name": "service-frontend",
247
+ "container": {
248
+ "image_name": "my-image-frontend"
249
+ }
250
+ },
251
+ "staging-backend-eu": {
252
+ "dimensions": {
253
+ "environment": "staging",
254
+ "service": "backend",
255
+ "region": "eu"
256
+ },
257
+ "registry": "gcr.io/my-project/",
258
+ "service_account": "my-service-account",
259
+ "name": "service-backend",
260
+ "container": {
261
+ "image_name": "my-image-backend",
262
+ "port": 8080,
263
+ "env": {
264
+ "ENABLE_EXPENSIVE_MONITORING": false
265
+ }
266
+ }
267
+ },
268
+ "dev-backend": {
269
+ "dimensions": {
270
+ "environment": "dev",
271
+ "service": "backend"
272
+ },
273
+ "registry": "gcr.io/my-project/",
274
+ "service_account": "my-service-account",
275
+ "name": "service-backend",
276
+ "container": {
277
+ "env": {
278
+ "DEBUG": true,
279
+ "ENABLE_EXPENSIVE_MONITORING": false
280
+ },
281
+ "image_name": "my-image-backend",
282
+ "port": 8080
283
+ }
284
+ }
197
285
  }
198
286
  ```
199
-
200
- You can pass either `config` (TOML string or dict) or `config_file` (`pathlib.Path` or string path) to `combine()`. Additional `kwargs` restrict the output.
@@ -6,62 +6,21 @@ service = ["api", "admin"]
6
6
  job = ["manage", "special-command"]
7
7
 
8
8
  [[output]]
9
- environment = "staging"
10
- stack = "next"
11
- type = "service"
12
-
13
- [[output]]
14
- environment = "staging"
15
- stack = "django"
16
- type = "service"
17
- service = "api"
18
-
19
- [[output]]
20
- environment = "staging"
21
- stack = "django"
22
- type = "service"
23
- service = "admin"
24
-
25
- [[output]]
26
- environment = "staging"
27
- stack = "django"
28
- type = "job"
29
- job = "manage"
30
-
31
- [[output]]
32
- environment = "staging"
33
- stack = "django"
34
- type = "job"
35
- job = "special-command"
36
-
37
- [[output]]
38
- environment = "production"
9
+ environment = ["staging", "production"]
39
10
  stack = "next"
40
11
  type = "service"
41
12
 
42
13
  [[output]]
43
- environment = "production"
44
- stack = "django"
45
- type = "service"
46
- service = "api"
47
-
48
- [[output]]
49
- environment = "production"
14
+ environment = ["staging", "production"]
50
15
  stack = "django"
51
16
  type = "service"
52
- service = "admin"
53
-
54
- [[output]]
55
- environment = "production"
56
- stack = "django"
57
- type = "job"
58
- job = "manage"
17
+ service = ["api", "admin"]
59
18
 
60
19
  [[output]]
61
- environment = "production"
20
+ environment = ["staging", "production"]
62
21
  stack = "django"
63
22
  type = "job"
64
- job = "special-command"
23
+ job = ["manage", "special-command"]
65
24
 
66
25
  [default]
67
26
  registry.region = "us"
@@ -107,7 +66,9 @@ cloudsql_instance = "staging-postgres"
107
66
  containers.app.env.APP_FOO = "qux"
108
67
 
109
68
  [[override]]
110
- when.service = "admin"
69
+ # The following line defines when in an array. It's not useful, as there's only one
70
+ # value, but we want to test that arrays work too.
71
+ when.service = ["admin"]
111
72
  containers.app.name = "admin"
112
73
  containers.app.env.APP_ADMIN_ENABLED = true
113
74
  containers.app.env.APP_ID = 1234
@@ -118,7 +118,7 @@ def test_generate_output(output: combiner.Output, expected: dict[str, int]):
118
118
 
119
119
  overrides = [
120
120
  combiner.Override(
121
- when={"env": "prod"},
121
+ when={"env": ["prod"]},
122
122
  config={
123
123
  "a": 10,
124
124
  "c": 30,
@@ -127,7 +127,7 @@ def test_generate_output(output: combiner.Output, expected: dict[str, int]):
127
127
  },
128
128
  ),
129
129
  combiner.Override(
130
- when={"env": "staging"},
130
+ when={"env": ["staging"]},
131
131
  config={
132
132
  "b": 200,
133
133
  "c": 300,
@@ -136,7 +136,7 @@ def test_generate_output(output: combiner.Output, expected: dict[str, int]):
136
136
  },
137
137
  ),
138
138
  combiner.Override(
139
- when={"env": "staging", "region": "us"},
139
+ when={"env": ["staging"], "region": ["us"]},
140
140
  config={"f": 5000, "g": 6000},
141
141
  ),
142
142
  ]
@@ -149,6 +149,46 @@ def test_generate_output(output: combiner.Output, expected: dict[str, int]):
149
149
  assert result == expected
150
150
 
151
151
 
152
+ @pytest.mark.parametrize(
153
+ "output, override, expected",
154
+ [
155
+ (
156
+ combiner.Output(dimensions={"env": "dev"}),
157
+ combiner.Override(when={"env": ["dev"]}, config={}),
158
+ True,
159
+ ),
160
+ (
161
+ combiner.Output(dimensions={"env": "dev"}),
162
+ combiner.Override(when={"env": ["staging"]}, config={}),
163
+ False,
164
+ ),
165
+ (
166
+ combiner.Output(dimensions={"env": "dev"}),
167
+ combiner.Override(when={"env": ["dev", "staging"]}, config={}),
168
+ True,
169
+ ),
170
+ (
171
+ combiner.Output(dimensions={"env": "staging"}),
172
+ combiner.Override(when={"region": ["us"]}, config={}),
173
+ False,
174
+ ),
175
+ (
176
+ combiner.Output(dimensions={"env": "dev"}),
177
+ combiner.Override(when={"env": ["dev"], "region": ["us"]}, config={}),
178
+ False,
179
+ ),
180
+ (
181
+ combiner.Output(dimensions={"env": "dev", "region": "us"}),
182
+ combiner.Override(when={"env": ["dev"]}, config={}),
183
+ True,
184
+ ),
185
+ ],
186
+ )
187
+ def test_output_matches_override(output, override, expected):
188
+ result = combiner.output_matches_override(output=output, override=override)
189
+ assert result == expected
190
+
191
+
152
192
  def test_build_config():
153
193
  raw_config = """
154
194
  [dimensions]
@@ -195,7 +235,7 @@ def test_build_config():
195
235
  config={"foo": "baz"},
196
236
  ),
197
237
  combiner.Override(
198
- when={"env": "prod"},
238
+ when={"env": ["prod"]},
199
239
  config={"foo": "qux"},
200
240
  ),
201
241
  ],
@@ -5,7 +5,7 @@ import dataclasses
5
5
  import itertools
6
6
  from collections.abc import Mapping, Sequence
7
7
  from functools import partial
8
- from typing import Any
8
+ from typing import Any, TypeVar, Union
9
9
 
10
10
  from . import exceptions
11
11
 
@@ -24,7 +24,7 @@ class Output:
24
24
 
25
25
  @dataclasses.dataclass()
26
26
  class Override:
27
- when: Mapping[str, str | list[str]]
27
+ when: Mapping[str, list[str]]
28
28
  config: Mapping[str, Any]
29
29
 
30
30
  def __str__(self) -> str:
@@ -48,9 +48,12 @@ def wrap_in_list(value: str | list[str]) -> list[str]:
48
48
  return value
49
49
 
50
50
 
51
+ T = TypeVar("T", bound=Union[str, list[str]])
52
+
53
+
51
54
  def clean_dimensions_dict(
52
- to_sort: Mapping[str, str | list[str]], clean: dict[str, list[str]], type: str
53
- ) -> dict[str, str]:
55
+ to_sort: Mapping[str, T], clean: dict[str, list[str]], type: str
56
+ ) -> dict[str, T]:
54
57
  """
55
58
  Recreate a dictionary of dimension values with the same order as the
56
59
  dimensions list.
@@ -148,7 +151,9 @@ def build_config(config: dict[str, Any]) -> Config:
148
151
  except KeyError:
149
152
  raise exceptions.MissingOverrideCondition(id=override)
150
153
 
151
- conditions = tuple((k, tuple(wrap_in_list(v))) for k, v in when.items())
154
+ when = {k: wrap_in_list(v) for k, v in when.items()}
155
+
156
+ conditions = tuple((k, tuple(v)) for k, v in when.items())
152
157
  if conditions in seen_conditions:
153
158
  raise exceptions.DuplicateError(type="override", id=when)
154
159
 
@@ -176,7 +181,6 @@ def build_config(config: dict[str, Any]) -> Config:
176
181
  output[key] = wrap_in_list(output[key])
177
182
 
178
183
  for cartesian_product in itertools.product(*output.values()):
179
- # Create a dictionary with the same keys as when
180
184
  single_output = dict(zip(output.keys(), cartesian_product))
181
185
 
182
186
  conditions = tuple(single_output.items())
@@ -200,6 +204,20 @@ def build_config(config: dict[str, Any]) -> Config:
200
204
  )
201
205
 
202
206
 
207
+ def output_matches_override(output: Output, override: Override) -> bool:
208
+ """
209
+ Check if the values in the override match the output dimensions.
210
+ """
211
+ for dim, values in override.when.items():
212
+ if dim not in output.dimensions:
213
+ return False
214
+
215
+ if output.dimensions[dim] not in values:
216
+ return False
217
+
218
+ return True
219
+
220
+
203
221
  def generate_output(
204
222
  default: Mapping[str, Any], overrides: Sequence[Override], output: Output
205
223
  ) -> dict[str, Any]:
@@ -207,10 +225,8 @@ def generate_output(
207
225
  # Apply each matching override
208
226
  for override in overrides:
209
227
  # Check if all dimension values in the override match
210
- if all(
211
- override.when.get(dim) == output.dimensions.get(dim)
212
- for dim in override.when.keys()
213
- ):
228
+
229
+ if output_matches_override(output=output, override=override):
214
230
  result = merge_configs(result, override.config)
215
231
 
216
232
  return {"dimensions": output.dimensions, **result}
File without changes