yaml-reference 0.3.1__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.
@@ -0,0 +1,8 @@
1
+ from yaml_reference.yaml import YAML
2
+
3
+ __all__ = ["YAML"]
4
+
5
+ # setattr(yaml, "load", recursively_resolve_after(yaml.load))
6
+ # setattr(yaml, "load_all", recursively_resolve_after(yaml.load_all))
7
+ # setattr(yaml, "dump", recursively_unresolve_before(yaml.dump))
8
+ # setattr(yaml, "dump_all", recursively_unresolve_before(yaml.dump_all))
@@ -0,0 +1,96 @@
1
+ import io
2
+ from typing import IO, Any
3
+
4
+ from ruamel.yaml import (
5
+ YAML,
6
+ DocumentEndEvent,
7
+ DocumentStartEvent,
8
+ Event,
9
+ MappingEndEvent,
10
+ MappingStartEvent,
11
+ ScalarEvent,
12
+ SequenceEndEvent,
13
+ SequenceStartEvent,
14
+ StreamEndEvent,
15
+ StreamStartEvent,
16
+ )
17
+
18
+
19
+ def load_anchor_from_file(yaml: YAML, stream: IO, anchor: str) -> Any:
20
+ """
21
+ Load a YAML file and return the data as a dictionary.
22
+
23
+ Args:
24
+ yaml (YAML): The YAML loader object.
25
+ stream (IO): A file-like object containing the YAML data.
26
+ anchor (str): The anchor to resolve.
27
+
28
+ Returns:
29
+ Any: The loaded YAML data.
30
+ """
31
+ if anchor is None:
32
+ raise ValueError("Anchor cannot be None")
33
+ level = 0
34
+ events: list[Event] = []
35
+ for event in yaml.parse(stream):
36
+ if isinstance(event, ScalarEvent) and event.anchor == anchor:
37
+ event.anchor = None
38
+ events = [event]
39
+ break
40
+ elif isinstance(event, MappingStartEvent) and event.anchor == anchor:
41
+ event.anchor = None
42
+ events = [event]
43
+ level = 1
44
+ elif isinstance(event, SequenceStartEvent) and event.anchor == anchor:
45
+ event.anchor = None
46
+ events = [event]
47
+ level = 1
48
+ elif level > 0:
49
+ events.append(event)
50
+ if isinstance(event, (MappingStartEvent, SequenceStartEvent)):
51
+ level += 1
52
+ elif isinstance(event, (MappingEndEvent, SequenceEndEvent)):
53
+ level -= 1
54
+ if level == 0:
55
+ break
56
+ if not events:
57
+ raise ValueError(f"Anchor '{anchor}' not found in {stream.name}")
58
+ events = [StreamStartEvent(), DocumentStartEvent()] + events + [DocumentEndEvent(), StreamEndEvent()]
59
+
60
+ # Ensure we inherit the "stream name"
61
+ strio = io.StringIO()
62
+ setattr(strio, "name", stream.name)
63
+ # Get a fresh YAML instance
64
+ _yaml = yaml.__class__()
65
+ _yaml.emit(events, strio)
66
+ strio.seek(0)
67
+ return yaml.load(strio)
68
+
69
+
70
+ def purge_anchors(yaml: YAML, stream: IO) -> IO:
71
+ """
72
+ Purge all anchors from the YAML stream.
73
+
74
+ Args:
75
+ yaml (YAML): The YAML loader object.
76
+ stream (IO): A file-like object containing the YAML data.
77
+
78
+ Returns:
79
+ IO: A file-like stream of YAML data with anchors removed.
80
+ """
81
+ events = list(yaml.parse(stream))
82
+ for i in range(len(events)):
83
+ if isinstance(events[i], ScalarEvent):
84
+ events[i].anchor = None
85
+ elif isinstance(events[i], MappingStartEvent):
86
+ events[i].anchor = None
87
+ elif isinstance(events[i], SequenceStartEvent):
88
+ events[i].anchor = None
89
+ # Ensure we inherit the "stream name"
90
+ strio = io.StringIO()
91
+ setattr(strio, "name", stream.name)
92
+ # Get a fresh YAML instance
93
+ _yaml = yaml.__class__()
94
+ _yaml.emit(events, strio)
95
+ strio.seek(0)
96
+ return strio
yaml_reference/cli.py ADDED
@@ -0,0 +1,47 @@
1
+ def compile_main(
2
+ input_file: str = None,
3
+ output_file: str = None,
4
+ ):
5
+ """
6
+ Compile a YAML file containing !reference tags into a new YAML file with resolved references.
7
+
8
+ Args:
9
+ input_file (str): The path to the input YAML file. If not provided, the function will read from standard input.
10
+ output_file (str): The path to the output YAML file. If not provided, the function will write to standard output.
11
+ """
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from yaml_reference import YAML
16
+
17
+ yaml = YAML()
18
+
19
+ if input_file is None:
20
+ input_file = sys.stdin.read()
21
+ input_file = Path(input_file)
22
+ else:
23
+ input_file = Path(input_file)
24
+
25
+ if output_file is None:
26
+ output_file = sys.stdout
27
+ else:
28
+ output_file = Path(output_file)
29
+
30
+ data = yaml.load(input_file)
31
+ yaml.dump(data, output_file)
32
+
33
+
34
+ def compile_cli():
35
+ import argparse
36
+
37
+ parser = argparse.ArgumentParser(
38
+ description="Compile a YAML file containing !reference tags into a new YAML file with resolved references."
39
+ )
40
+ parser.add_argument(
41
+ "-i", "--input", type=str, help="Path to the input YAML file. If not provided, reads from stdin."
42
+ )
43
+ parser.add_argument(
44
+ "-o", "--output", type=str, help="Path to the output YAML file. If not provided, writes to stdout."
45
+ )
46
+ args = parser.parse_args()
47
+ compile_main(args.input, args.output)
@@ -0,0 +1,10 @@
1
+ class ConstructorException(Exception):
2
+ """Exception raised when a constructor fails to construct an object."""
3
+
4
+ pass
5
+
6
+
7
+ class RepresenterException(Exception):
8
+ """Exception raised when a representer fails to represent an object."""
9
+
10
+ pass
@@ -0,0 +1,436 @@
1
+ from pathlib import Path
2
+ from typing import Any, Callable, Generic, Protocol, TypeVar, runtime_checkable
3
+
4
+ from ruamel.yaml import BaseConstructor, Constructor, MappingNode, Node, Representer
5
+
6
+ from yaml_reference import anchor
7
+ from yaml_reference.errors import ConstructorException, RepresenterException
8
+
9
+
10
+ def jmespath_search(data: Any, jmespath_expr: str) -> Any:
11
+ """
12
+ Perform a JMESPath search on the given data.
13
+
14
+ Args:
15
+ data (Any): The data to search.
16
+ jmespath_expr (str): The JMESPath expression to use for searching.
17
+
18
+ Returns:
19
+ Any: The result of the JMESPath search.
20
+ """
21
+ import jmespath
22
+
23
+ return jmespath.search(jmespath_expr, data)
24
+
25
+
26
+ T = TypeVar("T")
27
+
28
+
29
+ @runtime_checkable
30
+ class Resolvable(Protocol, Generic[T]):
31
+ """
32
+ A protocol that defines a method for resolving a reference.
33
+
34
+ Attributes:
35
+ __resolved__ (bool): Indicates whether the reference has been resolved.
36
+ __resolved_value__ (T): The resolved value of the reference.
37
+
38
+ Methods:
39
+ resolve() -> T: Resolves the reference.
40
+ """
41
+
42
+ __resolved__: bool
43
+ __resolved_value__: T
44
+
45
+ @property
46
+ def resolved(self) -> bool:
47
+ """
48
+ Indicates whether the reference has been resolved.
49
+
50
+ Returns:
51
+ bool: True if the reference is resolved, False otherwise.
52
+ """
53
+ return self.__resolved__
54
+
55
+ def resolve(self, yaml) -> T:
56
+ """
57
+ Resolves the reference.
58
+
59
+ Returns:
60
+ T: The resolved value of the reference.
61
+ """
62
+ pass
63
+
64
+
65
+ class Reference(Resolvable[Any]):
66
+ """
67
+ A class to represent a reference in a YAML file.
68
+
69
+ Attributes:
70
+ path (str): The path to the referenced file.
71
+ """
72
+
73
+ __local_file__: Path
74
+ path: Path
75
+ anchor: str | None # Optional anchor name to reference in the file
76
+ jmespath: str | None # Optional JMESPath expression to extract a specific value from the file
77
+
78
+ def __init__(self, local_file: Path, path: str, anchor: str | None = None, jmespath: str | None = None):
79
+ """
80
+ Initialize the Reference object with a path.
81
+
82
+ Args:
83
+ local_file (Path): The path to the local file containing the reference.
84
+ path (str): The path argument of the reference.
85
+ anchor (str, optional): The anchor name. Defaults to None.
86
+ jmespath (str, optional): The JMESPath expression. Defaults to None.
87
+ """
88
+ self.__resolved__ = False
89
+ self.__resolved_value__ = None
90
+ self.__local_file__ = local_file
91
+ self.path = local_file.parent / path
92
+ self.anchor = anchor
93
+ self.jmespath = jmespath
94
+
95
+ def __repr__(self) -> str:
96
+ """
97
+ Return a string representation of the Reference object.
98
+
99
+ Returns:
100
+ str: The string representation of the Reference object.
101
+ """
102
+ anchor_suffix = f"#{self.anchor}" if self.anchor else ""
103
+ jmespath_suffix = f", jmespath={self.jmespath}" if self.jmespath else ""
104
+ return f"Reference(path={self.path.relative_to(self.__local_file__.parent)}{anchor_suffix}{jmespath_suffix})"
105
+
106
+ def to_dict(self) -> dict[str, Any]:
107
+ """
108
+ Convert the Reference object to a dictionary.
109
+
110
+ Returns:
111
+ dict[str, Any]: The dictionary representation of the Reference object.
112
+ """
113
+ path_dict = {"path": str(self.path.relative_to(self.__local_file__.parent))}
114
+ if self.anchor:
115
+ path_dict["anchor"] = self.anchor
116
+ if self.jmespath:
117
+ path_dict["jmespath"] = self.jmespath
118
+ return path_dict
119
+
120
+ @classmethod
121
+ def from_yaml(cls, constructor: Constructor, node: Node) -> "Reference":
122
+ """
123
+ Create a Reference object from a YAML node.
124
+
125
+ Args:
126
+ constructor (Constructor): The YAML constructor.
127
+ node (Node): The YAML node.
128
+
129
+ Returns:
130
+ Reference: The created Reference object.
131
+ """
132
+ # local_file = Path(constructor.loader.reader.stream.name)
133
+ if not hasattr(constructor, "stream_name"):
134
+ raise ConstructorException("Constructor does not have a 'stream_name' attribute.")
135
+ local_file = Path(getattr(constructor, "stream_name", None))
136
+ if not isinstance(node, MappingNode):
137
+ raise ConstructorException(f"Invalid node type: {type(node)}")
138
+ dict_reference = BaseConstructor.construct_mapping(constructor, node)
139
+ return cls(local_file, **dict_reference)
140
+
141
+ @classmethod
142
+ def to_yaml(cls, representer: Representer, node: "Reference") -> Node:
143
+ """
144
+ Convert a Reference object to a YAML node.
145
+
146
+ Args:
147
+ representer (Representer): The YAML representer.
148
+ node (Reference): The Reference object.
149
+
150
+ Returns:
151
+ Node: The YAML node representing the Reference object.
152
+ """
153
+ if not isinstance(node, Reference):
154
+ raise RepresenterException(f"Invalid node type: {type(node)}")
155
+ return representer.represent_scalar("!reference", node.to_dict(), style="flow")
156
+
157
+ def resolve(self, loader: Any) -> Any:
158
+ """
159
+ Resolve the reference and return the resolved value.
160
+
161
+ Args:
162
+ loader (YAML): The YAML loader.
163
+
164
+ Returns:
165
+ Any: The resolved value of the reference.
166
+ """
167
+ if self.resolved:
168
+ return self.__resolved_value__
169
+
170
+ try:
171
+ if self.anchor:
172
+ data = anchor.load_anchor_from_file(loader, self.path.open("r"), self.anchor)
173
+ else:
174
+ data = loader.load(self.path.open("r"))
175
+ if self.jmespath:
176
+ data = jmespath_search(data, self.jmespath)
177
+ except ImportError as e:
178
+ raise ConstructorException(
179
+ "JMESPath expression is not supported because the 'jmespath' package is not installed.\n" + str(e)
180
+ ) from e
181
+ except Exception as e:
182
+ raise ConstructorException(f"Failed to resolve reference: {self.path.absolute()}\nException:\n{e}") from e
183
+ # setattr(data, "__resolvable__", self)
184
+ self.__resolved_value__ = data
185
+ self.__resolved__ = True
186
+ return self.__resolved_value__
187
+
188
+
189
+ class ReferenceAll(Resolvable[list[Any]]):
190
+ """
191
+ A class to represent a reference to all YAML files matching a glob pattern.
192
+
193
+ Attributes:
194
+ path (str): The path to the referenced file.
195
+ """
196
+
197
+ __local_file__: Path
198
+ glob: str
199
+ anchor: str | None # Optional anchor name to reference in each of the files
200
+ jmespath: str | None # Optional JMESPath expression to extract a specific value from each of the files
201
+ paths: list[Path] # List of paths matching the glob pattern
202
+
203
+ def __init__(self, local_file: Path, glob: str, anchor: str | None = None, jmespath: str | None = None):
204
+ """
205
+ Initialize the ReferenceAll object with a glob pattern.
206
+
207
+ Args:
208
+ local_file (Path): The path to the local file containing the reference.
209
+ glob (str): The glob pattern to match files.
210
+ anchor (str, optional): The anchor name. Defaults to None.
211
+ jmespath (str, optional): The JMESPath expression. Defaults to None.
212
+ """
213
+ self.__resolved__ = False
214
+ self.__resolved_value__ = None
215
+ self.__local_file__ = local_file
216
+ self.glob = glob
217
+ self.paths = list(local_file.parent.glob(glob))
218
+ self.anchor = anchor
219
+ self.jmespath = jmespath
220
+
221
+ def __repr__(self) -> str:
222
+ """
223
+ Return a string representation of the ReferenceAll object.
224
+
225
+ Returns:
226
+ str: The string representation of the ReferenceAll object.
227
+ """
228
+ anchor_suffix = f"#{self.anchor}" if self.anchor else ""
229
+ jmespath_suffix = f", jmespath={self.jmespath}" if self.jmespath else ""
230
+ return f"ReferenceAll(glob={self.glob}{anchor_suffix}{jmespath_suffix})"
231
+
232
+ def to_dict(self) -> dict[str, Any]:
233
+ """
234
+ Convert the ReferenceAll object to a dictionary.
235
+
236
+ Returns:
237
+ dict[str, Any]: The dictionary representation of the ReferenceAll object.
238
+ """
239
+ glob_dict = {"glob": str(self.glob)}
240
+ if self.anchor:
241
+ glob_dict["anchor"] = self.anchor
242
+ return glob_dict
243
+
244
+ @classmethod
245
+ def from_yaml(cls, constructor: Constructor, node: Node) -> "ReferenceAll":
246
+ """
247
+ Create a ReferenceAll object from a YAML node.
248
+
249
+ Args:
250
+ constructor (Constructor): The YAML constructor.
251
+ node (Node): The YAML node.
252
+
253
+ Returns:
254
+ ReferenceAll: The created ReferenceAll object.
255
+ """
256
+ # local_file = Path(constructor.loader.reader.stream.name)
257
+ if not hasattr(constructor, "stream_name"):
258
+ raise ConstructorException("Constructor does not have a 'stream_name' attribute.")
259
+ local_file = Path(getattr(constructor, "stream_name", None))
260
+ if not isinstance(node, MappingNode):
261
+ raise ConstructorException(f"Invalid node type: {type(node)}")
262
+ dict_reference = BaseConstructor.construct_mapping(constructor, node)
263
+ return cls(local_file, **dict_reference)
264
+
265
+ @classmethod
266
+ def to_yaml(cls, representer: Representer, node: "ReferenceAll") -> Node:
267
+ """
268
+ Convert a ReferenceAll object to a YAML node.
269
+
270
+ Args:
271
+ representer (Representer): The YAML representer.
272
+ node (ReferenceAll): The ReferenceAll object.
273
+
274
+ Returns:
275
+ Node: The YAML node representing the ReferenceAll object.
276
+ """
277
+ if not isinstance(node, ReferenceAll):
278
+ raise RepresenterException(f"Invalid node type: {type(node)}")
279
+ return representer.represent_scalar("!reference-all", node.to_dict(), style="flow")
280
+
281
+ def resolve(self, loader: Any) -> Any:
282
+ """
283
+ Resolve the reference and return the resolved value.
284
+
285
+ Args:
286
+ loader (YAML): The YAML loader.
287
+
288
+ Returns:
289
+ Any: The resolved value of the reference.
290
+ """
291
+ if self.resolved:
292
+ return self.__resolved_value__
293
+
294
+ data = []
295
+ for path in self.paths:
296
+ # we need to purge anchors from all loaded results.
297
+ anchored_yaml_stream = path.open("r")
298
+ anchorless_yaml_stream = anchor.purge_anchors(loader, path.open("r"))
299
+ next_data = (
300
+ anchor.load_anchor_from_file(loader, anchored_yaml_stream, self.anchor)
301
+ if self.anchor
302
+ else loader.load(anchorless_yaml_stream)
303
+ )
304
+ if self.jmespath:
305
+ next_data = jmespath_search(next_data, self.jmespath)
306
+ data.append(next_data)
307
+ if not data:
308
+ raise ConstructorException(f"Failed to resolve reference: {self.glob}")
309
+ # setattr(data, "__resolvable__", self)
310
+ self.__resolved_value__ = data
311
+ self.__resolved__ = True
312
+ return self.__resolved_value__
313
+
314
+
315
+ def resolve(yaml, data: Any) -> Any:
316
+ """
317
+ Resolve a reference.
318
+
319
+ Args:
320
+ yaml (YAML): The YAML loader.
321
+ data (Any): The reference to resolve.
322
+
323
+ Returns:
324
+ Any: The resolved data.
325
+ """
326
+ if hasattr(data, "__resolved__") and hasattr(data, "resolve"):
327
+ return data.resolve(yaml)
328
+ return data
329
+
330
+
331
+ def recursively_resolve(yaml: Any, data: Any) -> Any:
332
+ """
333
+ Recursively resolve all references in the given data.
334
+
335
+ Args:
336
+ yaml (YAML): The YAML loader.
337
+ data (Any): The data to resolve references in.
338
+
339
+ Returns:
340
+ Any: The data with all references resolved.
341
+ """
342
+ try:
343
+ if isinstance(data, list):
344
+ return [recursively_resolve(yaml, item) for item in data]
345
+ elif isinstance(data, dict):
346
+ return {key: recursively_resolve(yaml, value) for key, value in data.items()}
347
+ elif isinstance(data, Resolvable):
348
+ return resolve(yaml, data)
349
+ else:
350
+ return data
351
+ except ConstructorException as e:
352
+ raise ConstructorException(f"Error resolving reference: {e}") from e
353
+ except Exception as e:
354
+ raise ConstructorException(f"Unexpected error: {e}") from e
355
+
356
+
357
+ def recursively_resolve_after(yaml, func: Callable) -> Callable:
358
+ """Decorator to resolve data after a function call.
359
+
360
+ Args:
361
+ yaml (YAML): The YAML loader.
362
+ func (Callable): Function to be decorated.
363
+
364
+ Returns:
365
+ Callable: Decorated function.
366
+ """
367
+
368
+ def wrapper(*args, **kwargs):
369
+ result = func(*args, **kwargs)
370
+ return recursively_resolve(yaml, result)
371
+
372
+ return wrapper
373
+
374
+
375
+ ##
376
+ ## Eventually, I'll figure out how to round-trip references and I'll use some sort of "unresolve" API.
377
+ ##
378
+
379
+
380
+ def unresolve(data: Any) -> Any:
381
+ """
382
+ Unresolve a resolved value.
383
+
384
+ Args:
385
+ data (Any): The resolved value to unresolve.
386
+
387
+ Returns:
388
+ Any: The unresolved data.
389
+ """
390
+ if hasattr(data, "__resolvable__"):
391
+ return data.__resolvable__
392
+ return data
393
+
394
+
395
+ def recursively_unresolve(data: Any) -> Any:
396
+ """
397
+ Recursively unresolve all references in the given data.
398
+
399
+ Args:
400
+ data (Any): The data to unresolve references in.
401
+
402
+ Returns:
403
+ Any: The data with all references unresolved.
404
+ """
405
+ try:
406
+ if isinstance(data, list):
407
+ return [recursively_unresolve(item) for item in data]
408
+ elif isinstance(data, dict):
409
+ return {key: recursively_unresolve(value) for key, value in data.items()}
410
+ elif isinstance(data, Resolvable):
411
+ return unresolve(data)
412
+ else:
413
+ return data
414
+ except RepresenterException as e:
415
+ raise RepresenterException(f"Error unresolving reference: {e}") from e
416
+ except Exception as e:
417
+ raise RepresenterException(f"Unexpected error: {e}") from e
418
+
419
+
420
+ def recursively_unresolve_before(func: Callable) -> Callable:
421
+ """Decorator to unresolve data before a function call.
422
+
423
+ Args:
424
+ func (Callable): Function to be decorated.
425
+
426
+ Returns:
427
+ Callable: Decorated function.
428
+ """
429
+
430
+ def wrapper(*args, **kwargs):
431
+ result = recursively_unresolve(args[0])
432
+ args = (result,) + args[1:]
433
+ return func(*args, **kwargs)
434
+
435
+ return wrapper
436
+ return wrapper
yaml_reference/yaml.py ADDED
@@ -0,0 +1,47 @@
1
+ from typing import IO, Callable
2
+
3
+ from ruamel.yaml import YAML as _YAML
4
+ from ruamel.yaml import Constructor, RoundTripConstructor
5
+
6
+ from yaml_reference.reference import (
7
+ Reference,
8
+ ReferenceAll,
9
+ recursively_resolve_after,
10
+ recursively_unresolve_before,
11
+ )
12
+
13
+
14
+ def _attach_stream_name_to_constructor(yaml, func: Callable) -> Callable:
15
+ def wrapper(*args, **kwargs):
16
+ stream = args[0] if args else kwargs.get("stream")
17
+ yaml.constructor.stream_name = stream.name
18
+ yaml.Constructor.stream_name = stream.name
19
+ rval = func(*args, **kwargs)
20
+ yaml.constructor.stream_name = None
21
+ yaml.Constructor.stream_name = None
22
+ return rval
23
+
24
+ return wrapper
25
+
26
+
27
+ class YAML(_YAML):
28
+ """
29
+ A class to represent a YAML object with custom loading and dumping behavior.
30
+ """
31
+
32
+ def __init__(self, *args, **kwargs):
33
+ super().__init__(*args, **kwargs)
34
+ self.constructor.add_constructor("!reference", Reference.from_yaml)
35
+ self.constructor.add_constructor("!reference-all", ReferenceAll.from_yaml)
36
+ # I'm not sure which of these I should use, so I'll attach the state to both.
37
+ setattr(self.constructor, "stream_name", None)
38
+ setattr(self.Constructor, "stream_name", None)
39
+ self.load = _attach_stream_name_to_constructor(self, recursively_resolve_after(self, self.load))
40
+ self.load_all = _attach_stream_name_to_constructor(self, recursively_resolve_after(self, self.load_all))
41
+ # self.representer.add_representer(Reference, Reference.to_yaml)
42
+ # self.representer.add_representer(ReferenceAll, ReferenceAll.to_yaml)
43
+ # self.dump = recursively_unresolve_before(self.dump)
44
+ # self.dump_all = recursively_unresolve_before(self.dump_all)
45
+
46
+
47
+ __all__ = ["YAML"]
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.3
2
+ Name: yaml-reference
3
+ Version: 0.3.1
4
+ Summary:
5
+ Author: David Sillman
6
+ Author-email: dsillman2000@gmail.com
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: jmespath (>=1.0.1,<2.0.0)
15
+ Requires-Dist: ruamel-yaml (>=0.18.11,<0.19.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # yaml-reference
19
+
20
+ Using `ruamel.yaml`, support cross-file references in YAML files using tags `!reference` and `!reference-all`.
21
+
22
+ ## Example
23
+
24
+ ```yaml
25
+ # root.yaml
26
+ version: "3.1"
27
+ services:
28
+ - !reference
29
+ path: "services/website.yaml"
30
+
31
+ - !reference
32
+ path: "services/database.yaml"
33
+
34
+ networkConfigs:
35
+ !reference-all
36
+ glob: "networks/*.yaml"
37
+
38
+ ```
39
+
40
+ Supposing there are `services/website.yaml` and `services/database.yaml` files in the same directory as `root.yaml`, and a `networks` directory with YAML files, the above will be expanded to account for the referenced files with the following Python code:
41
+
42
+ ```python
43
+ from yaml_reference import YAML
44
+
45
+ yaml = YAML()
46
+ with open("root.yaml", "r") as f:
47
+ data = yaml.load(f)
48
+ ```
49
+
50
+ Note that the `YAML` class is a direct subclass of the base `ruamel.yaml.YAML` loader class, so the same API applies for customizing how it loads YAML files or other tags (e.g. `yaml = YAML(typ='safe')`).
51
+
52
+ ## CLI interface
53
+
54
+ There is a CLI interface for this package which can be used to convert a YAML file which contains `!reference` tags into a single YAML file with all the references expanded. This is useful for generating a single file for deployment or other purposes.
55
+
56
+ ```bash
57
+ $ yref-compile -h
58
+ usage: yref-compile [-h] [-i INPUT] [-o OUTPUT]
59
+
60
+ Compile a YAML file containing !reference tags into a new YAML file with resolved references.
61
+
62
+ options:
63
+ -h, --help show this help message and exit
64
+ -i INPUT, --input INPUT
65
+ Path to the input YAML file. If not provided, reads from stdin.
66
+ -o OUTPUT, --output OUTPUT
67
+ Path to the output YAML file. If not provided, writes to stdout.
68
+ $ yref-compile -i root.yaml
69
+ version: '3.1'
70
+ services:
71
+ - website
72
+ - database
73
+ networkConfigs:
74
+ - network: vpn
75
+ version: 1.1
76
+ - network: nfs
77
+ version: 1.0
78
+ ```
79
+
80
+ ## Anchor references
81
+
82
+ You can supply the `!reference` / `!reference-all` tags with an anchor name to use for the reference.
83
+
84
+ ```yaml
85
+ # root.yaml
86
+ ports:
87
+ !reference-all
88
+ glob: "networks/*.yaml"
89
+ anchor: "port"
90
+ ```
91
+
92
+ ```yaml
93
+ # networks/vpn.yaml
94
+ name: vpn
95
+ port: &port 8001
96
+ ```
97
+
98
+ ```yaml
99
+ # networks/nfs.yaml
100
+ name: nfs
101
+ port: &port 2000
102
+ ```
103
+
104
+ Loading the `root.yaml` file with the Python interface or converting it with the CLI will result in the following YAML (in no particular order):
105
+
106
+ ```yaml
107
+ ports:
108
+ - 8001
109
+ - 2000
110
+ ```
111
+
112
+ ## JMESPath functionality
113
+
114
+ You can also use JMESPath expressions to filter the results of references:
115
+
116
+ ```yaml
117
+ # furthest.yml
118
+ furthest-town-name:
119
+ !reference
120
+ path: "towns/all.yml"
121
+ jmespath: "max_by(towns, &distance).name"
122
+ ```
123
+
124
+ ```yaml
125
+ #towns/all.yml
126
+ towns:
127
+ !reference-all
128
+ glob: "towns/*.yml"
129
+ ```
130
+
131
+ ```yaml
132
+ # towns/los_altos.yml
133
+ name: Los Altos
134
+ distance: 10
135
+ # towns/sunnyvale.yml
136
+ name: Sunnyvale
137
+ distance: 5
138
+ # towns/mountain_view.yml
139
+ name: Mountain View
140
+ distance: 15
141
+ ```
142
+
143
+ Using the CLI or Python interface for loading the root `furthes.yml` file will yield the following result:
144
+
145
+ ```yaml
146
+ furthest-town-name: Mountain View
147
+ ```
148
+
149
+ See more information about JMESPath expressions in the [JMESPath documentation](https://jmespath.org/).
150
+
151
+ ## Acknowledgements
152
+
153
+ Author(s):
154
+
155
+ - David Sillman <dsillman2000@gmail.com>
156
+ - Personal website: https://www.dsillman.com
157
+
@@ -0,0 +1,10 @@
1
+ yaml_reference/__init__.py,sha256=HyIgDU13Yi4sS8ubNmUsk6yoetuGAP8K05stDbEuuuY,328
2
+ yaml_reference/anchor.py,sha256=GbaNgoyy-NNSzRHkse3nb9eqWrcCNAR_ZRvZ0LKcRb8,2907
3
+ yaml_reference/cli.py,sha256=jIMcepK4xzLdf_okklRZlojqO5pTaAZXX_KWevEgtTc,1427
4
+ yaml_reference/errors.py,sha256=mrx2JpCPPA9zCLHLh_t7Wfuw6Ec0tMeKagAodvqYDQ4,252
5
+ yaml_reference/reference.py,sha256=EZrEHa_EFuFW6LVU3snTnzZEHoD_fiHqDNDn5__YDzU,14294
6
+ yaml_reference/yaml.py,sha256=pDXY4wj33l-hdaEF0AB8XgrJHpB4lPx6i-FW57rHZOQ,1785
7
+ yaml_reference-0.3.1.dist-info/METADATA,sha256=1QpbShqmFsaH3MD-zXlqMB9jy-rKS8MTUD20rDoqhqw,3831
8
+ yaml_reference-0.3.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
9
+ yaml_reference-0.3.1.dist-info/entry_points.txt,sha256=BtGQpEHredeEjZrRp1IpeDPavol7YOuLF2aN2xw7pqI,63
10
+ yaml_reference-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ yref-compile=yaml_reference.cli:compile_cli
3
+