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 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
@@ -0,0 +1,6 @@
1
+ """Allow running the package via ``python -m eksp``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
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)