eksp 0.9.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.
- eksp/__init__.py +61 -0
- eksp/__main__.py +6 -0
- eksp/api.py +92 -0
- eksp/cli.py +180 -0
- eksp/json_format.py +18 -0
- eksp/namespaces.py +606 -0
- eksp/pipeline.py +80 -0
- eksp/resolver.py +1066 -0
- eksp/sample.py +192 -0
- eksp-0.9.0.dist-info/METADATA +639 -0
- eksp-0.9.0.dist-info/RECORD +14 -0
- eksp-0.9.0.dist-info/WHEEL +4 -0
- eksp-0.9.0.dist-info/entry_points.txt +3 -0
- eksp-0.9.0.dist-info/licenses/LICENSE +21 -0
eksp/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""eksp - resolve Jinja-style ``{{ ... }}`` references in JSON configurations.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
- :func:`eksp.resolver.resolve` - recursive fixed-point Jinja rendering and
|
|
6
|
+
``$include`` / ``$include.*`` / ``$src`` / ``$src.*`` handling (optional ``debug=True`` for
|
|
7
|
+
``//$include*`` and ``//$src*`` diagnostics).
|
|
8
|
+
- :func:`eksp.namespaces.build_namespaces` - build the lookup namespaces from
|
|
9
|
+
``--src``, ``--ctxt``, ``--srcvar``, ``--var`` inputs, and the process environment.
|
|
10
|
+
- :func:`eksp.namespaces.parse_ctxt_arg` - parse one ``--ctxt`` path (same rules as ``--src``).
|
|
11
|
+
- :func:`eksp.namespaces.parse_srcvar_arg` - parse one ``--srcvar`` key (path into ``SRC``).
|
|
12
|
+
- :func:`eksp.namespaces.parse_var_arg` - parse one ``--var`` key (path into ``CTXT``).
|
|
13
|
+
- :func:`eksp.namespaces.parse_path_prefix` - parse ``--srcvar-root`` / ``--var-root``.
|
|
14
|
+
- :func:`eksp.cli.main` - the ``eksp`` console script entry point.
|
|
15
|
+
- :func:`eksp.api.resolve_cli` - parse CLI-style ``argv``; return
|
|
16
|
+
``(resolved, namespaces)`` tuple.
|
|
17
|
+
- :func:`eksp.pipeline.build_and_resolve` - same pipeline as the CLI without
|
|
18
|
+
parsing flags or serializing JSON.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .api import resolve_cli
|
|
22
|
+
from .namespaces import (
|
|
23
|
+
RESERVED_LABELS,
|
|
24
|
+
build_namespaces,
|
|
25
|
+
parse_ctxt_arg,
|
|
26
|
+
parse_path_prefix,
|
|
27
|
+
parse_src_arg,
|
|
28
|
+
parse_srcvar_arg,
|
|
29
|
+
parse_var_arg,
|
|
30
|
+
prepend_path_root,
|
|
31
|
+
)
|
|
32
|
+
from .pipeline import build_and_resolve
|
|
33
|
+
from .resolver import (
|
|
34
|
+
INCLUDE_KEY,
|
|
35
|
+
MAX_ITERATIONS,
|
|
36
|
+
SRC_KEY,
|
|
37
|
+
ResolutionError,
|
|
38
|
+
make_native_environment,
|
|
39
|
+
resolve,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"INCLUDE_KEY",
|
|
44
|
+
"MAX_ITERATIONS",
|
|
45
|
+
"RESERVED_LABELS",
|
|
46
|
+
"SRC_KEY",
|
|
47
|
+
"ResolutionError",
|
|
48
|
+
"build_and_resolve",
|
|
49
|
+
"build_namespaces",
|
|
50
|
+
"make_native_environment",
|
|
51
|
+
"parse_ctxt_arg",
|
|
52
|
+
"parse_path_prefix",
|
|
53
|
+
"parse_src_arg",
|
|
54
|
+
"parse_srcvar_arg",
|
|
55
|
+
"parse_var_arg",
|
|
56
|
+
"prepend_path_root",
|
|
57
|
+
"resolve",
|
|
58
|
+
"resolve_cli",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
__version__ = "0.9.0"
|
eksp/__main__.py
ADDED
eksp/api.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Programmatic entry points that mirror the ``eksp`` CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .pipeline import build_and_resolve
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resolve_cli(
|
|
15
|
+
argv: str | Sequence[str] | None = None,
|
|
16
|
+
*,
|
|
17
|
+
environ: Mapping[str, str] | None = None,
|
|
18
|
+
) -> tuple[Any, dict[str, Any]]:
|
|
19
|
+
"""Parse CLI-style arguments and return ``(resolved, namespaces)``.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
argv:
|
|
24
|
+
Arguments as for :func:`subprocess.run`: a single string (split with
|
|
25
|
+
:func:`shlex.split`), a sequence of tokens, or ``None`` to use
|
|
26
|
+
``sys.argv[1:]``. Do not include the program name.
|
|
27
|
+
environ:
|
|
28
|
+
Optional environment mapping for the ``ENV`` namespace. Defaults to
|
|
29
|
+
``os.environ`` when building namespaces.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
tuple[Any, dict[str, Any]]
|
|
34
|
+
The resolved merged ``SRC`` document and the full namespace mapping from
|
|
35
|
+
:func:`eksp.namespaces.build_namespaces`.
|
|
36
|
+
|
|
37
|
+
Warnings
|
|
38
|
+
--------
|
|
39
|
+
Emits :class:`UserWarning` if ``--output``, ``--compact``, or ``--sort-keys``
|
|
40
|
+
are present; those flags only affect JSON serialization in the CLI.
|
|
41
|
+
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
click.exceptions.ClickException
|
|
45
|
+
On invalid CLI usage (including ``--help`` / ``--version``, which exit
|
|
46
|
+
via Click as in the terminal).
|
|
47
|
+
NamespaceError
|
|
48
|
+
Invalid ``--src``, ``--ctxt``, ``--srcvar``, or ``--var`` arguments.
|
|
49
|
+
ResolutionError
|
|
50
|
+
Template or resolution failure while rendering.
|
|
51
|
+
"""
|
|
52
|
+
if argv is None:
|
|
53
|
+
args = sys.argv[1:]
|
|
54
|
+
elif isinstance(argv, str):
|
|
55
|
+
args = shlex.split(argv)
|
|
56
|
+
else:
|
|
57
|
+
args = list(argv)
|
|
58
|
+
|
|
59
|
+
from .cli import main
|
|
60
|
+
|
|
61
|
+
ctx = main.make_context("eksp", args)
|
|
62
|
+
params = ctx.params
|
|
63
|
+
|
|
64
|
+
if params.get("output") is not None:
|
|
65
|
+
warnings.warn(
|
|
66
|
+
"--output is ignored by resolve_cli (the result is returned, not written to a file)",
|
|
67
|
+
UserWarning,
|
|
68
|
+
stacklevel=2,
|
|
69
|
+
)
|
|
70
|
+
if params.get("compact"):
|
|
71
|
+
warnings.warn(
|
|
72
|
+
"--compact is ignored by resolve_cli",
|
|
73
|
+
UserWarning,
|
|
74
|
+
stacklevel=2,
|
|
75
|
+
)
|
|
76
|
+
if params.get("sort_keys"):
|
|
77
|
+
warnings.warn(
|
|
78
|
+
"--sort-keys is ignored by resolve_cli",
|
|
79
|
+
UserWarning,
|
|
80
|
+
stacklevel=2,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return build_and_resolve(
|
|
84
|
+
src_args=params["src_args"],
|
|
85
|
+
ctxt_args=params["ctxt_args"],
|
|
86
|
+
srcvar_args=params["srcvar_args"],
|
|
87
|
+
var_args=params["var_args"],
|
|
88
|
+
srcvar_root=params.get("srcvar_root"),
|
|
89
|
+
var_root=params.get("var_root"),
|
|
90
|
+
debug=params["debug"],
|
|
91
|
+
environ=environ,
|
|
92
|
+
)
|
eksp/cli.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""``eksp`` command-line entry point.
|
|
2
|
+
|
|
3
|
+
Run ``eksp --help`` for the full reference. The CLI's job is to translate
|
|
4
|
+
``--src``, ``--ctxt``, ``--srcvar``, and ``--var`` arguments into namespaces,
|
|
5
|
+
resolve the merged ``SRC`` document, and serialize the result back to JSON.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .json_format import format_json
|
|
17
|
+
from .namespaces import NamespaceError
|
|
18
|
+
from .pipeline import build_and_resolve
|
|
19
|
+
from .resolver import ResolutionError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command(
|
|
23
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
24
|
+
epilog=(
|
|
25
|
+
"Examples:\n"
|
|
26
|
+
"\n"
|
|
27
|
+
" eksp --src cfg1.json --src cfg2.json \\\n"
|
|
28
|
+
" --var name1=abc --var name2=def\n"
|
|
29
|
+
"\n"
|
|
30
|
+
" eksp --src cfg.json --var solo=pat\n"
|
|
31
|
+
" (patches merged CTXT, e.g. {{ CTXT.solo }})\n"
|
|
32
|
+
" eksp --src R1=cfg1.json --src R2=cfg2.json \\\n"
|
|
33
|
+
" --var name1=abc\n"
|
|
34
|
+
"\n"
|
|
35
|
+
"Variables are referenced with Jinja's {{ NAMESPACE.path }} syntax, e.g.\n"
|
|
36
|
+
"{{ ENV.HOME }}, {{ SRC.db.host }}, {{ CTXT1.fragment }}, {{ CTXT.solo }}.\n"
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"--src",
|
|
41
|
+
"src_args",
|
|
42
|
+
metavar="[LABEL=]PATH",
|
|
43
|
+
multiple=True,
|
|
44
|
+
help=(
|
|
45
|
+
"Load JSON from PATH. With LABEL=, expose as namespace LABEL (repeat LABEL "
|
|
46
|
+
"to shallow-merge several files into that label) and as SRC# for this line. "
|
|
47
|
+
"With PATH only, expose only as SRC#. Every file is shallow-merged into SRC "
|
|
48
|
+
"(later files win at the top level). Repeatable."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--ctxt",
|
|
53
|
+
"ctxt_args",
|
|
54
|
+
metavar="[LABEL=]PATH",
|
|
55
|
+
multiple=True,
|
|
56
|
+
help=(
|
|
57
|
+
"Load JSON like --src; shallow-merge into CTXT and expose as CTXT1, "
|
|
58
|
+
"CTXT2, … (repeat LABEL to merge into that label). Not merged into SRC or "
|
|
59
|
+
"printed output. Repeatable."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
@click.option(
|
|
63
|
+
"--srcvar",
|
|
64
|
+
"srcvar_args",
|
|
65
|
+
metavar="PATH.TO.KEY=VALUE",
|
|
66
|
+
multiple=True,
|
|
67
|
+
help=(
|
|
68
|
+
"Set a string on the merged SRC document: Jinja-style path (.a, ['b']) "
|
|
69
|
+
"before the first =; value is the rest (may contain =). Repeatable."
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--srcvar-root",
|
|
74
|
+
"srcvar_root",
|
|
75
|
+
metavar="PATH",
|
|
76
|
+
default=None,
|
|
77
|
+
help=(
|
|
78
|
+
"Prepend PATH. to every --srcvar key (dot-separated Jinja-style path, "
|
|
79
|
+
"same rules as --srcvar keys without =)."
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--var",
|
|
84
|
+
"var_args",
|
|
85
|
+
metavar="PATH.TO.KEY=VALUE",
|
|
86
|
+
multiple=True,
|
|
87
|
+
help=(
|
|
88
|
+
"Patch a string into the merged CTXT document: Jinja-style path (.a, ['b']) "
|
|
89
|
+
"before the first =; value is the rest (may contain =). Not merged into "
|
|
90
|
+
"printed SRC. Repeatable."
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--var-root",
|
|
95
|
+
"var_root",
|
|
96
|
+
metavar="PATH",
|
|
97
|
+
default=None,
|
|
98
|
+
help=(
|
|
99
|
+
"Prepend PATH. to every --var key (dot-separated Jinja-style path, same "
|
|
100
|
+
"rules as --var keys without =)."
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"--output",
|
|
105
|
+
"-o",
|
|
106
|
+
"output",
|
|
107
|
+
type=click.Path(dir_okay=False, writable=True, path_type=Path),
|
|
108
|
+
default=None,
|
|
109
|
+
help="Write the resolved JSON to PATH instead of stdout.",
|
|
110
|
+
)
|
|
111
|
+
@click.option(
|
|
112
|
+
"--compact",
|
|
113
|
+
is_flag=True,
|
|
114
|
+
default=False,
|
|
115
|
+
help="Emit compact JSON (one line, no extra whitespace).",
|
|
116
|
+
)
|
|
117
|
+
@click.option(
|
|
118
|
+
"--sort-keys",
|
|
119
|
+
is_flag=True,
|
|
120
|
+
default=False,
|
|
121
|
+
help="Sort object keys in JSON output (default preserves source key order).",
|
|
122
|
+
)
|
|
123
|
+
@click.option(
|
|
124
|
+
"--debug",
|
|
125
|
+
is_flag=True,
|
|
126
|
+
default=False,
|
|
127
|
+
help=(
|
|
128
|
+
"Keep $include / $include.* source values under //$include / //$include.* "
|
|
129
|
+
"and src diagnostics under //$src, //$src.*, and //$ctxt.*."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
@click.version_option(__version__, prog_name="eksp")
|
|
133
|
+
def main(
|
|
134
|
+
src_args: tuple[str, ...],
|
|
135
|
+
ctxt_args: tuple[str, ...],
|
|
136
|
+
srcvar_args: tuple[str, ...],
|
|
137
|
+
srcvar_root: str | None,
|
|
138
|
+
var_args: tuple[str, ...],
|
|
139
|
+
var_root: str | None,
|
|
140
|
+
output: Path | None,
|
|
141
|
+
compact: bool,
|
|
142
|
+
sort_keys: bool,
|
|
143
|
+
debug: bool,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Resolve Jinja-style ``{{ ... }}`` references in JSON configurations.
|
|
146
|
+
|
|
147
|
+
eksp loads zero or more JSON files via ``--src`` (``PATH`` or ``LABEL=PATH``)
|
|
148
|
+
and zero or more via ``--ctxt`` (same shape, exposed as ``CTXT1``, ``CTXT2``,
|
|
149
|
+
… without merging into ``SRC``). ``--srcvar`` patches string values into the
|
|
150
|
+
merged ``SRC`` object; ``--var`` patches the merged ``CTXT`` namespace for templates.
|
|
151
|
+
The ``SRC`` namespace is then rendered recursively until every string stops
|
|
152
|
+
changing (and ``$include`` / ``$include.*`` keys are expanded). The result is
|
|
153
|
+
printed as JSON.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
resolved, _namespaces = build_and_resolve(
|
|
157
|
+
src_args,
|
|
158
|
+
ctxt_args,
|
|
159
|
+
srcvar_args,
|
|
160
|
+
var_args,
|
|
161
|
+
srcvar_root=srcvar_root,
|
|
162
|
+
var_root=var_root,
|
|
163
|
+
debug=debug,
|
|
164
|
+
)
|
|
165
|
+
except NamespaceError as exc:
|
|
166
|
+
raise click.UsageError(str(exc)) from exc
|
|
167
|
+
except ResolutionError as exc:
|
|
168
|
+
raise click.ClickException(str(exc)) from exc
|
|
169
|
+
|
|
170
|
+
text = format_json(resolved, compact=compact, sort_keys=sort_keys)
|
|
171
|
+
|
|
172
|
+
if output is None:
|
|
173
|
+
click.echo(text)
|
|
174
|
+
else:
|
|
175
|
+
output.write_text(text + ("" if text.endswith("\n") else "\n"), encoding="utf-8")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__": # pragma: no cover
|
|
179
|
+
main(prog_name="eksp")
|
|
180
|
+
sys.exit(0)
|
eksp/json_format.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Serialize resolved values as JSON for the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_json(value: Any, *, compact: bool = False, sort_keys: bool = False) -> str:
|
|
10
|
+
"""Return ``value`` as JSON text.
|
|
11
|
+
|
|
12
|
+
By default, object keys keep their insertion order (Python 3.7+ dicts).
|
|
13
|
+
Set ``sort_keys`` to emit keys in sorted order at every object level.
|
|
14
|
+
"""
|
|
15
|
+
kwargs: dict[str, Any] = {"ensure_ascii": False, "sort_keys": sort_keys}
|
|
16
|
+
if compact:
|
|
17
|
+
return json.dumps(value, separators=(",", ":"), **kwargs)
|
|
18
|
+
return json.dumps(value, indent=2, **kwargs)
|