toml-combine 0.1.4__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/__init__.py +55 -0
- toml_combine/__main__.py +6 -0
- toml_combine/cli.py +86 -0
- toml_combine/combiner.py +249 -0
- toml_combine/exceptions.py +31 -0
- toml_combine/toml.py +17 -0
- toml_combine-0.1.4.dist-info/METADATA +210 -0
- toml_combine-0.1.4.dist-info/RECORD +10 -0
- toml_combine-0.1.4.dist-info/WHEEL +4 -0
- toml_combine-0.1.4.dist-info/entry_points.txt +2 -0
toml_combine/__init__.py
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import pathlib
|
4
|
+
from typing import Any, cast, overload
|
5
|
+
|
6
|
+
from . import combiner, toml
|
7
|
+
|
8
|
+
|
9
|
+
# Provide already parsed config
|
10
|
+
@overload
|
11
|
+
def combine(
|
12
|
+
*, config: dict[str, Any], **filters: str | list[str]
|
13
|
+
) -> dict[str, Any]: ...
|
14
|
+
# Provide toml config content
|
15
|
+
@overload
|
16
|
+
def combine(*, config: str, **filters: str | list[str]) -> dict[str, Any]: ...
|
17
|
+
# Provide toml config file path
|
18
|
+
@overload
|
19
|
+
def combine(
|
20
|
+
*, config_file: str | pathlib.Path, **filters: str | list[str]
|
21
|
+
) -> dict[str, Any]: ...
|
22
|
+
|
23
|
+
|
24
|
+
def combine(*, config=None, config_file=None, **filters):
|
25
|
+
"""
|
26
|
+
Generate outputs of configurations based on the provided TOML
|
27
|
+
configuration.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
config: The TOML configuration as a string or an already parsed dictionary.
|
31
|
+
OR:
|
32
|
+
config_file: The path to the TOML configuration file.
|
33
|
+
**filters: Filters to apply to the combined configuration
|
34
|
+
(dimension="value" or dimension=["list", "of", "values"]).
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
dict[str, Any]: The combined configuration ({"output_id": {...}}).
|
38
|
+
"""
|
39
|
+
if (config is None) is (config_file is None):
|
40
|
+
raise ValueError("Either 'config' or 'config_file' must be provided.")
|
41
|
+
|
42
|
+
if isinstance(config, dict):
|
43
|
+
dict_config = config
|
44
|
+
else:
|
45
|
+
if config_file:
|
46
|
+
config_string = pathlib.Path(config_file).read_text()
|
47
|
+
else:
|
48
|
+
config = cast(str, config)
|
49
|
+
config_string = config
|
50
|
+
|
51
|
+
dict_config = toml.loads(config_string)
|
52
|
+
|
53
|
+
config_obj = combiner.build_config(dict_config)
|
54
|
+
|
55
|
+
return combiner.generate_outputs(config_obj, **filters)
|
toml_combine/__main__.py
ADDED
toml_combine/cli.py
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import json
|
5
|
+
import pathlib
|
6
|
+
import sys
|
7
|
+
from collections.abc import Mapping
|
8
|
+
|
9
|
+
from . import combiner, exceptions, toml
|
10
|
+
|
11
|
+
|
12
|
+
def get_argument_parser(
|
13
|
+
dimensions: Mapping[str, list[str]] | None,
|
14
|
+
) -> argparse.ArgumentParser:
|
15
|
+
"""Get the command-line argument parser."""
|
16
|
+
arg_parser = argparse.ArgumentParser(
|
17
|
+
description="Create combined configurations from a TOML file",
|
18
|
+
add_help=(dimensions is not None),
|
19
|
+
)
|
20
|
+
arg_parser.add_argument(
|
21
|
+
"config",
|
22
|
+
type=pathlib.Path,
|
23
|
+
help="Path to the TOML configuration file",
|
24
|
+
)
|
25
|
+
if dimensions:
|
26
|
+
group = arg_parser.add_argument_group(
|
27
|
+
"dimensions",
|
28
|
+
"Filter the generated outputs by dimensions",
|
29
|
+
)
|
30
|
+
|
31
|
+
for name, values in dimensions.items():
|
32
|
+
group.add_argument(
|
33
|
+
f"--{name}", choices=values, help=f"Limit to given {name}"
|
34
|
+
)
|
35
|
+
|
36
|
+
return arg_parser
|
37
|
+
|
38
|
+
|
39
|
+
def cli(argv) -> int:
|
40
|
+
"""Main entry point."""
|
41
|
+
|
42
|
+
# Parse the config file argument to get the dimensions
|
43
|
+
arg_parser = get_argument_parser(dimensions=None)
|
44
|
+
args, _ = arg_parser.parse_known_args(argv)
|
45
|
+
|
46
|
+
try:
|
47
|
+
dict_config = toml.loads(args.config.read_text())
|
48
|
+
except FileNotFoundError:
|
49
|
+
print(f"Configuration file not found: {args.config}", file=sys.stderr)
|
50
|
+
return 1
|
51
|
+
except exceptions.TomlDecodeError as exc:
|
52
|
+
print(exc, file=sys.stderr)
|
53
|
+
return 1
|
54
|
+
|
55
|
+
try:
|
56
|
+
config = combiner.build_config(dict_config)
|
57
|
+
except exceptions.TomlCombineError as exc:
|
58
|
+
print(exc, file=sys.stderr)
|
59
|
+
return 1
|
60
|
+
|
61
|
+
# Parse all arguments
|
62
|
+
arg_parser = get_argument_parser(dimensions=config.dimensions)
|
63
|
+
args = arg_parser.parse_args(argv)
|
64
|
+
|
65
|
+
dimensions_filter = {
|
66
|
+
key: value
|
67
|
+
for key, value in vars(args).items()
|
68
|
+
if key in config.dimensions and value
|
69
|
+
}
|
70
|
+
# Generate final configurations for each output
|
71
|
+
try:
|
72
|
+
result = combiner.generate_outputs(config=config, **dimensions_filter)
|
73
|
+
except exceptions.TomlCombineError as exc:
|
74
|
+
print(exc, file=sys.stderr)
|
75
|
+
return 1
|
76
|
+
|
77
|
+
if not result:
|
78
|
+
print("No outputs found", file=sys.stderr)
|
79
|
+
|
80
|
+
print(json.dumps(result, indent=2))
|
81
|
+
|
82
|
+
return 0
|
83
|
+
|
84
|
+
|
85
|
+
def run_cli():
|
86
|
+
sys.exit(cli(sys.argv[1:]))
|
toml_combine/combiner.py
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import copy
|
4
|
+
import dataclasses
|
5
|
+
import itertools
|
6
|
+
from collections.abc import Mapping, Sequence
|
7
|
+
from functools import partial
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from . import exceptions
|
11
|
+
|
12
|
+
|
13
|
+
@dataclasses.dataclass()
|
14
|
+
class Output:
|
15
|
+
dimensions: Mapping[str, str]
|
16
|
+
|
17
|
+
@property
|
18
|
+
def id(self) -> str:
|
19
|
+
return f"{'-'.join(self.dimensions.values())}"
|
20
|
+
|
21
|
+
def __str__(self) -> str:
|
22
|
+
return f"Output(id={self.id})"
|
23
|
+
|
24
|
+
|
25
|
+
@dataclasses.dataclass()
|
26
|
+
class Override:
|
27
|
+
when: Mapping[str, str | list[str]]
|
28
|
+
config: Mapping[str, Any]
|
29
|
+
|
30
|
+
def __str__(self) -> str:
|
31
|
+
return f"Override({self.when})"
|
32
|
+
|
33
|
+
|
34
|
+
@dataclasses.dataclass()
|
35
|
+
class Config:
|
36
|
+
dimensions: Mapping[str, list[str]]
|
37
|
+
outputs: list[Output]
|
38
|
+
default: Mapping[str, Any]
|
39
|
+
overrides: Sequence[Override]
|
40
|
+
|
41
|
+
|
42
|
+
def wrap_in_list(value: str | list[str]) -> list[str]:
|
43
|
+
"""
|
44
|
+
Wrap a string in a list if it's not already a list.
|
45
|
+
"""
|
46
|
+
if isinstance(value, str):
|
47
|
+
return [value]
|
48
|
+
return value
|
49
|
+
|
50
|
+
|
51
|
+
def clean_dimensions_dict(
|
52
|
+
to_sort: Mapping[str, str | list[str]], clean: dict[str, list[str]], type: str
|
53
|
+
) -> dict[str, str]:
|
54
|
+
"""
|
55
|
+
Recreate a dictionary of dimension values with the same order as the
|
56
|
+
dimensions list.
|
57
|
+
"""
|
58
|
+
result = {}
|
59
|
+
remaining = dict(to_sort)
|
60
|
+
|
61
|
+
for dimension, valid_values in clean.items():
|
62
|
+
valid_values = set(valid_values)
|
63
|
+
if dimension not in to_sort:
|
64
|
+
continue
|
65
|
+
|
66
|
+
original_value = remaining.pop(dimension)
|
67
|
+
values = set(wrap_in_list(original_value))
|
68
|
+
if invalid_values := values - valid_values:
|
69
|
+
raise exceptions.DimensionValueNotFound(
|
70
|
+
type=type,
|
71
|
+
id=to_sort,
|
72
|
+
dimension=dimension,
|
73
|
+
value=", ".join(invalid_values),
|
74
|
+
)
|
75
|
+
result[dimension] = original_value
|
76
|
+
|
77
|
+
if remaining:
|
78
|
+
raise exceptions.DimensionNotFound(
|
79
|
+
type=type,
|
80
|
+
id=to_sort,
|
81
|
+
dimension=", ".join(to_sort),
|
82
|
+
)
|
83
|
+
|
84
|
+
return result
|
85
|
+
|
86
|
+
|
87
|
+
def override_sort_key(
|
88
|
+
override: Override, dimensions: dict[str, list[str]]
|
89
|
+
) -> tuple[int, ...]:
|
90
|
+
"""
|
91
|
+
We sort overrides before applying them, and they are applied in the order of the
|
92
|
+
sorted list, each override replacing the common values of the previous overrides.
|
93
|
+
|
94
|
+
override_sort_key defines the sort key for overrides that ensures less specific
|
95
|
+
overrides come first:
|
96
|
+
- Overrides with fewer dimensions come first (will be overridden
|
97
|
+
by more specific ones)
|
98
|
+
- If two overrides have the same number of dimensions but define different
|
99
|
+
dimensions, we sort by the definition order of the dimensions.
|
100
|
+
|
101
|
+
Example:
|
102
|
+
dimensions = {"env": ["dev", "prod"], "region": ["us", "eu"]}
|
103
|
+
|
104
|
+
- Override with {"env": "dev"} comes before override with
|
105
|
+
{"env": "dev", "region": "us"} (less specific)
|
106
|
+
- Override with {"env": "dev"} comes before override with {"region": "us"} ("env"
|
107
|
+
is defined before "region" in the dimensions list)
|
108
|
+
"""
|
109
|
+
result = [len(override.when)]
|
110
|
+
for i, dimension in enumerate(dimensions):
|
111
|
+
if dimension in override.when:
|
112
|
+
result.append(i)
|
113
|
+
|
114
|
+
return tuple(result)
|
115
|
+
|
116
|
+
|
117
|
+
def merge_configs(a: Any, b: Any, /) -> Any:
|
118
|
+
"""
|
119
|
+
Recursively merge two configuration dictionaries, with b taking precedence.
|
120
|
+
"""
|
121
|
+
if isinstance(a, dict) != isinstance(b, dict):
|
122
|
+
raise ValueError(f"Cannot merge {type(a)} with {type(b)}")
|
123
|
+
|
124
|
+
if not isinstance(a, dict):
|
125
|
+
return b
|
126
|
+
|
127
|
+
result = a.copy()
|
128
|
+
for key, b_value in b.items():
|
129
|
+
if a_value := a.get(key):
|
130
|
+
result[key] = merge_configs(a_value, b_value)
|
131
|
+
else:
|
132
|
+
result[key] = b_value
|
133
|
+
return result
|
134
|
+
|
135
|
+
|
136
|
+
def build_config(config: dict[str, Any]) -> Config:
|
137
|
+
# Parse dimensions
|
138
|
+
dimensions = config.pop("dimensions")
|
139
|
+
|
140
|
+
# Parse template
|
141
|
+
default = config.pop("default", {})
|
142
|
+
|
143
|
+
seen_conditions = set()
|
144
|
+
overrides = []
|
145
|
+
for override in config.pop("override", []):
|
146
|
+
try:
|
147
|
+
when = override.pop("when")
|
148
|
+
except KeyError:
|
149
|
+
raise exceptions.MissingOverrideCondition(id=override)
|
150
|
+
|
151
|
+
conditions = tuple((k, tuple(wrap_in_list(v))) for k, v in when.items())
|
152
|
+
if conditions in seen_conditions:
|
153
|
+
raise exceptions.DuplicateError(type="override", id=when)
|
154
|
+
|
155
|
+
seen_conditions.add(conditions)
|
156
|
+
|
157
|
+
overrides.append(
|
158
|
+
Override(
|
159
|
+
when=clean_dimensions_dict(
|
160
|
+
to_sort=when, clean=dimensions, type="override"
|
161
|
+
),
|
162
|
+
config=override,
|
163
|
+
)
|
164
|
+
)
|
165
|
+
# Sort overrides by increasing specificity
|
166
|
+
overrides = sorted(
|
167
|
+
overrides,
|
168
|
+
key=partial(override_sort_key, dimensions=dimensions),
|
169
|
+
)
|
170
|
+
|
171
|
+
outputs = []
|
172
|
+
seen_conditions = set()
|
173
|
+
|
174
|
+
for output in config.pop("output", []):
|
175
|
+
for key in output:
|
176
|
+
output[key] = wrap_in_list(output[key])
|
177
|
+
|
178
|
+
for cartesian_product in itertools.product(*output.values()):
|
179
|
+
# Create a dictionary with the same keys as when
|
180
|
+
single_output = dict(zip(output.keys(), cartesian_product))
|
181
|
+
|
182
|
+
conditions = tuple(single_output.items())
|
183
|
+
if conditions in seen_conditions:
|
184
|
+
raise exceptions.DuplicateError(type="output", id=output.id)
|
185
|
+
seen_conditions.add(conditions)
|
186
|
+
|
187
|
+
outputs.append(
|
188
|
+
Output(
|
189
|
+
dimensions=clean_dimensions_dict(
|
190
|
+
single_output, dimensions, type="output"
|
191
|
+
),
|
192
|
+
)
|
193
|
+
)
|
194
|
+
|
195
|
+
return Config(
|
196
|
+
dimensions=dimensions,
|
197
|
+
outputs=outputs,
|
198
|
+
default=default,
|
199
|
+
overrides=overrides,
|
200
|
+
)
|
201
|
+
|
202
|
+
|
203
|
+
def generate_output(
|
204
|
+
default: Mapping[str, Any], overrides: Sequence[Override], output: Output
|
205
|
+
) -> dict[str, Any]:
|
206
|
+
result = copy.deepcopy(default)
|
207
|
+
# Apply each matching override
|
208
|
+
for override in overrides:
|
209
|
+
# 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
|
+
):
|
214
|
+
result = merge_configs(result, override.config)
|
215
|
+
|
216
|
+
return {"dimensions": output.dimensions, **result}
|
217
|
+
|
218
|
+
|
219
|
+
def generate_outputs(config: Config, **filter: str | list[str]) -> dict[str, Any]:
|
220
|
+
result = {}
|
221
|
+
filter_with_lists: dict[str, list[str]] = {}
|
222
|
+
|
223
|
+
for key, value in list(filter.items()):
|
224
|
+
if key not in config.dimensions:
|
225
|
+
raise exceptions.DimensionNotFound(type="arguments", id="", dimension=key)
|
226
|
+
|
227
|
+
value = wrap_in_list(value)
|
228
|
+
|
229
|
+
if set(value) - set(config.dimensions[key]):
|
230
|
+
raise exceptions.DimensionValueNotFound(
|
231
|
+
type="arguments",
|
232
|
+
id="",
|
233
|
+
dimension=key,
|
234
|
+
value=", ".join(set(value) - set(config.dimensions[key])),
|
235
|
+
)
|
236
|
+
filter_with_lists[key] = value
|
237
|
+
|
238
|
+
for output in config.outputs:
|
239
|
+
if all(
|
240
|
+
output.dimensions.get(key) in value
|
241
|
+
for key, value in filter_with_lists.items()
|
242
|
+
):
|
243
|
+
result[output.id] = generate_output(
|
244
|
+
default=config.default,
|
245
|
+
overrides=config.overrides,
|
246
|
+
output=output,
|
247
|
+
)
|
248
|
+
|
249
|
+
return result
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
|
4
|
+
class TomlCombineError(Exception):
|
5
|
+
"""There was an error in the toml-combine library."""
|
6
|
+
|
7
|
+
def __init__(self, message: str = "", **kwargs) -> None:
|
8
|
+
message = message or type(self).__doc__ or ""
|
9
|
+
message = message.format(**kwargs)
|
10
|
+
|
11
|
+
super().__init__(message)
|
12
|
+
|
13
|
+
|
14
|
+
class TomlDecodeError(TomlCombineError):
|
15
|
+
"""Error while decoding configuration file."""
|
16
|
+
|
17
|
+
|
18
|
+
class DuplicateError(TomlCombineError):
|
19
|
+
"""In {type} {id}: Cannot have multiple {type}s with the same dimensions."""
|
20
|
+
|
21
|
+
|
22
|
+
class DimensionNotFound(TomlCombineError):
|
23
|
+
"""In {type} {id}: Dimension {dimension} not found."""
|
24
|
+
|
25
|
+
|
26
|
+
class DimensionValueNotFound(TomlCombineError):
|
27
|
+
"""In {type} {id}: Value {value} for dimension {dimension} not found."""
|
28
|
+
|
29
|
+
|
30
|
+
class MissingOverrideCondition(TomlCombineError):
|
31
|
+
"""In override {id}: Missing 'when' key in override configuration"""
|
toml_combine/toml.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import tomli
|
6
|
+
|
7
|
+
from . import exceptions
|
8
|
+
|
9
|
+
|
10
|
+
def loads(raw_config: str) -> dict[str, Any]:
|
11
|
+
try:
|
12
|
+
return tomli.loads(raw_config)
|
13
|
+
except tomli.TOMLDecodeError as e:
|
14
|
+
raise exceptions.TomlDecodeError(
|
15
|
+
message="Syntax error in TOML configuration file",
|
16
|
+
context=str(e),
|
17
|
+
) from e
|
@@ -0,0 +1,210 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: toml-combine
|
3
|
+
Version: 0.1.4
|
4
|
+
Summary: A tool for combining complex configurations in TOML format.
|
5
|
+
Author-email: Joachim Jablon <ewjoachim@gmail.com>
|
6
|
+
License-Expression: MIT
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
8
|
+
Classifier: Intended Audience :: Developers
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
16
|
+
Requires-Python: >=3.9
|
17
|
+
Requires-Dist: tomli>=2.2.1
|
18
|
+
Description-Content-Type: text/markdown
|
19
|
+
|
20
|
+
# Toml-combine
|
21
|
+
|
22
|
+
`toml-combine` is a Python lib and CLI-tool that reads a toml configuration defining
|
23
|
+
a default configuration and overrides with, and applies those overrides to get
|
24
|
+
final configurations. Say: you have multiple services, and environments, and you
|
25
|
+
want to describe them all without repeating the parts that are common to everyone.
|
26
|
+
|
27
|
+
## Concepts
|
28
|
+
|
29
|
+
### The config file
|
30
|
+
|
31
|
+
The configuration file is usually a TOML file. Here's a small example:
|
32
|
+
|
33
|
+
```toml
|
34
|
+
[dimensions]
|
35
|
+
environment = ["production", "staging"]
|
36
|
+
|
37
|
+
[[output]]
|
38
|
+
environment = "production"
|
39
|
+
|
40
|
+
[[output]]
|
41
|
+
environment = "staging"
|
42
|
+
|
43
|
+
[default]
|
44
|
+
name = "my-service"
|
45
|
+
registry = "gcr.io/my-project/"
|
46
|
+
container.image_name = "my-image"
|
47
|
+
container.port = 8080
|
48
|
+
service_account = "my-service-account"
|
49
|
+
|
50
|
+
[[override]]
|
51
|
+
when.environment = "staging"
|
52
|
+
service_account = "my-staging-service-account"
|
53
|
+
```
|
54
|
+
|
55
|
+
### Dimensions
|
56
|
+
|
57
|
+
Consider all the configurations you want to generate. Each one differs from the others.
|
58
|
+
Dimensions lets you describe the main "thing" that makes manifests differents, e.g.:
|
59
|
+
`environment` might be `staging` or `production`, region might be `eu` or `us`, and
|
60
|
+
service might be `frontend` or `backend`. Some combinations of dimensions might not
|
61
|
+
exists, for example, maybe there's no `staging` in `eu`.
|
62
|
+
|
63
|
+
### Outputs
|
64
|
+
|
65
|
+
Create a `output` for each configuration you want to generate, and specify the
|
66
|
+
dimensions relevant for this output. It's ok to omit some dimensions when they're not
|
67
|
+
used for a given output.
|
68
|
+
|
69
|
+
> [!Note]
|
70
|
+
> Defining a list as the value of one or more dimensions in a output
|
71
|
+
> is a shorthand for defining all combinations of dimensions
|
72
|
+
|
73
|
+
### Default
|
74
|
+
|
75
|
+
The common configuration to start from, before we start overlaying overrides on top.
|
76
|
+
|
77
|
+
### Overrides
|
78
|
+
|
79
|
+
Overrides define a set of condition where they apply (`when.<dimension> =
|
80
|
+
"<value>"`) and the values that are overriden. Overrides are applied in order from less
|
81
|
+
specific to more specific, each one overriding the values of the previous ones:
|
82
|
+
|
83
|
+
- If an override contains conditions on more dimensions than another one, it's applied
|
84
|
+
later
|
85
|
+
- In case 2 overrides contain the same number of dimensions and they're a disjoint set,
|
86
|
+
then it depends on how the dimensions are defined at the top of the file: dimensions
|
87
|
+
defined last have a greater priority
|
88
|
+
|
89
|
+
> [!Note]
|
90
|
+
> Defining a list as the value of one or more conditions in an override
|
91
|
+
> means that the override will apply to any of the dimension values of the list
|
92
|
+
|
93
|
+
### The configuration itself
|
94
|
+
|
95
|
+
Under the layer of `dimensions/output/default/override` system, what you actually define
|
96
|
+
in the configuration is completely up to you. That said, only nested
|
97
|
+
"dictionnaries"/"objects"/"tables"/"mapping" (those are all the same things in
|
98
|
+
Python/JS/Toml lingo) will be merged between the default and the overrides, while
|
99
|
+
arrays will just replace one another. See `Arrays` below.
|
100
|
+
|
101
|
+
In the generated configuration, the dimensions of the output will appear in the generated
|
102
|
+
object as an object under the `dimensions` key.
|
103
|
+
|
104
|
+
### Arrays
|
105
|
+
|
106
|
+
Let's look at an example:
|
107
|
+
|
108
|
+
```toml
|
109
|
+
[dimensions]
|
110
|
+
environment = ["production", "staging"]
|
111
|
+
|
112
|
+
[[output]]
|
113
|
+
environment = ["production", "staging"]
|
114
|
+
|
115
|
+
[default]
|
116
|
+
fruits = [{name="apple", color="red"}]
|
117
|
+
|
118
|
+
[[override]]
|
119
|
+
when.environment = "staging"
|
120
|
+
fruits = [{name="orange", color="orange"}]
|
121
|
+
```
|
122
|
+
|
123
|
+
In this example, on staging, `fruits` is `[{name="orange", color="orange"}]` and not `[{name="apple", color="red"}, {name="orange", color="orange"}]`.
|
124
|
+
The only way to get multiple values to be merged is if they are tables: you'll need
|
125
|
+
to chose an element to become the key:
|
126
|
+
|
127
|
+
```toml
|
128
|
+
[dimensions]
|
129
|
+
environment = ["production", "staging"]
|
130
|
+
|
131
|
+
[[output]]
|
132
|
+
environment = ["production", "staging"]
|
133
|
+
|
134
|
+
[default]
|
135
|
+
fruits.apple.color = "red"
|
136
|
+
|
137
|
+
[[override]]
|
138
|
+
when.environment = "staging"
|
139
|
+
fruits.orange.color = "orange"
|
140
|
+
```
|
141
|
+
|
142
|
+
In this example, on staging, `fruits` is `{apple={color="red"}, orange={color="orange"}}`.
|
143
|
+
|
144
|
+
This example is simple because `name` is a natural choice for the key. In some cases,
|
145
|
+
the choice is less natural, but you can always decide to name the elements of your
|
146
|
+
list and use that name as a key. Also, yes, you'll loose ordering.
|
147
|
+
|
148
|
+
### A bigger example
|
149
|
+
|
150
|
+
```toml
|
151
|
+
[dimensions]
|
152
|
+
environment = ["production", "staging", "dev"]
|
153
|
+
service = ["frontend", "backend"]
|
154
|
+
|
155
|
+
# All 4 combinations of those values will exist
|
156
|
+
[[output]]
|
157
|
+
environment = ["production", "staging"]
|
158
|
+
service = ["frontend", "backend"]
|
159
|
+
|
160
|
+
# On dev, the "service" is not defined. That's ok.
|
161
|
+
[[output]]
|
162
|
+
environment = "dev"
|
163
|
+
|
164
|
+
[default]
|
165
|
+
registry = "gcr.io/my-project/"
|
166
|
+
service_account = "my-service-account"
|
167
|
+
|
168
|
+
[[override]]
|
169
|
+
when.service = "frontend"
|
170
|
+
name = "service-frontend"
|
171
|
+
container.image_name = "my-image-frontend"
|
172
|
+
|
173
|
+
[[override]]
|
174
|
+
when.service = "backend"
|
175
|
+
name = "service-backend"
|
176
|
+
container.image_name = "my-image-backend"
|
177
|
+
container.port = 8080
|
178
|
+
|
179
|
+
[[override]]
|
180
|
+
name = "service-dev"
|
181
|
+
when.environment = "dev"
|
182
|
+
container.env.DEBUG = true
|
183
|
+
```
|
184
|
+
|
185
|
+
### CLI
|
186
|
+
|
187
|
+
```console
|
188
|
+
$ toml-combine {path/to/config.toml}
|
189
|
+
```
|
190
|
+
|
191
|
+
Generates all the outputs described by the given TOML config.
|
192
|
+
|
193
|
+
Note that you can restrict generation to some dimension values by passing
|
194
|
+
`--{dimension}={value}`
|
195
|
+
|
196
|
+
## Lib
|
197
|
+
|
198
|
+
```python
|
199
|
+
import toml_combine
|
200
|
+
|
201
|
+
|
202
|
+
result = toml_combine.combine(
|
203
|
+
config_file=config_file,
|
204
|
+
environment=["production", "staging"],
|
205
|
+
type="job",
|
206
|
+
job=["manage", "special-command"],
|
207
|
+
)
|
208
|
+
|
209
|
+
print(result)
|
210
|
+
```
|
@@ -0,0 +1,10 @@
|
|
1
|
+
toml_combine/__init__.py,sha256=l7i0GkM9k7cc__wj1yqK5XjpB3IJ0jqFU63tqKMuYlY,1625
|
2
|
+
toml_combine/__main__.py,sha256=hmF8N8xX6UEApzbKTVZ-4E1HU5-rjgUkdXNLO-mF6vo,100
|
3
|
+
toml_combine/cli.py,sha256=MZrAEP4wt6f9Qn0TEXIjeLoQMlvQulFpkMciwU8GRO4,2328
|
4
|
+
toml_combine/combiner.py,sha256=98iWUnkbzDVGEkw-dXFZEt5G7io5SQzayceuTU2hRvo,7315
|
5
|
+
toml_combine/exceptions.py,sha256=SepRFDxeWQEbD88jhF5g7laZSSULthho83BpW8u9RWs,897
|
6
|
+
toml_combine/toml.py,sha256=_vCINvfJeS3gWid35Pmm3Yz4xyJ8LpKJRHL0axSU8nk,384
|
7
|
+
toml_combine-0.1.4.dist-info/METADATA,sha256=_IzI_kjsNl52wFynzgII2zXeMGnD8XqJ8Mw5FHwOLIs,6051
|
8
|
+
toml_combine-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
+
toml_combine-0.1.4.dist-info/entry_points.txt,sha256=dXUQNom54uZt_7ylEG81iNYMamYpaFo9-ItcZJU6Uzc,58
|
10
|
+
toml_combine-0.1.4.dist-info/RECORD,,
|