nyl 0.10.4__tar.gz → 0.26.0__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.
Files changed (149) hide show
  1. nyl-0.26.0/.gitignore +30 -0
  2. nyl-0.26.0/PKG-INFO +152 -0
  3. nyl-0.26.0/README.md +140 -0
  4. nyl-0.26.0/pyproject.toml +50 -0
  5. nyl-0.26.0/src/nyl/__init__.py +34 -0
  6. nyl-0.26.0/src/nyl/_version.py +1 -0
  7. nyl-0.26.0/src/nyl/client.py +113 -0
  8. nyl-0.26.0/src/nyl/errors.py +54 -0
  9. nyl-0.26.0/src/nyl/manifests.py +267 -0
  10. nyl-0.10.4/.changelog/0.10.0.toml +0 -31
  11. nyl-0.10.4/.changelog/0.10.1.toml +0 -13
  12. nyl-0.10.4/.changelog/0.10.2.toml +0 -31
  13. nyl-0.10.4/.changelog/0.10.3.toml +0 -13
  14. nyl-0.10.4/.changelog/0.7.0.toml +0 -70
  15. nyl-0.10.4/.changelog/0.7.1.toml +0 -7
  16. nyl-0.10.4/.changelog/0.7.2.toml +0 -31
  17. nyl-0.10.4/.changelog/0.8.0.toml +0 -7
  18. nyl-0.10.4/.changelog/0.8.1.toml +0 -7
  19. nyl-0.10.4/.changelog/0.9.0.toml +0 -31
  20. nyl-0.10.4/.changelog/0.9.1.toml +0 -25
  21. nyl-0.10.4/.changelog/0.9.2.toml +0 -13
  22. nyl-0.10.4/.changelog/0.9.3.toml +0 -7
  23. nyl-0.10.4/.changelog/0.9.4.toml +0 -7
  24. nyl-0.10.4/.envrc +0 -5
  25. nyl-0.10.4/.github/workflows/docker.yaml +0 -55
  26. nyl-0.10.4/.github/workflows/docs.yaml +0 -40
  27. nyl-0.10.4/.github/workflows/python.yaml +0 -87
  28. nyl-0.10.4/.github/workflows/update-flake-lock.yaml +0 -23
  29. nyl-0.10.4/.gitignore +0 -14
  30. nyl-0.10.4/PKG-INFO +0 -67
  31. nyl-0.10.4/README.md +0 -44
  32. nyl-0.10.4/argocd-cmp/Dockerfile +0 -111
  33. nyl-0.10.4/argocd-cmp/README.md +0 -11
  34. nyl-0.10.4/argocd-cmp/plugin.yaml +0 -47
  35. nyl-0.10.4/argocd-with-nyl/.envrc +0 -7
  36. nyl-0.10.4/argocd-with-nyl/.secrets.yaml +0 -29
  37. nyl-0.10.4/argocd-with-nyl/.sops.yaml +0 -5
  38. nyl-0.10.4/argocd-with-nyl/README.md +0 -51
  39. nyl-0.10.4/argocd-with-nyl/argocd.yaml +0 -84
  40. nyl-0.10.4/argocd-with-nyl/nyl-project.yaml +0 -1
  41. nyl-0.10.4/argocd-with-nyl/nyl-secrets.yaml +0 -2
  42. nyl-0.10.4/docs/content/changelog.md +0 -208
  43. nyl-0.10.4/docs/content/index.md +0 -22
  44. nyl-0.10.4/docs/content/reference/applysets.md +0 -10
  45. nyl-0.10.4/docs/content/reference/argocd-plugin.md +0 -254
  46. nyl-0.10.4/docs/content/reference/cluster-connectivity.md +0 -27
  47. nyl-0.10.4/docs/content/reference/configuration/environment.md +0 -71
  48. nyl-0.10.4/docs/content/reference/configuration/profiles.md +0 -241
  49. nyl-0.10.4/docs/content/reference/configuration/projects.md +0 -89
  50. nyl-0.10.4/docs/content/reference/configuration/secrets.md +0 -152
  51. nyl-0.10.4/docs/content/reference/configuration-api/profiles.md +0 -6
  52. nyl-0.10.4/docs/content/reference/configuration-api/secrets.md +0 -11
  53. nyl-0.10.4/docs/content/reference/glossary.md +0 -28
  54. nyl-0.10.4/docs/content/reference/templating/basics.md +0 -21
  55. nyl-0.10.4/docs/content/reference/templating/components.md +0 -83
  56. nyl-0.10.4/docs/content/reference/templating/inlining/helmcharts.md +0 -158
  57. nyl-0.10.4/docs/content/reference/templating/inlining/overview.md +0 -10
  58. nyl-0.10.4/docs/content/reference/templating/inlining/postprocessor.md +0 -156
  59. nyl-0.10.4/docs/content/reference/templating/locals.md +0 -26
  60. nyl-0.10.4/docs/content/reference/templating/secrets.md +0 -37
  61. nyl-0.10.4/docs/mkdocs.yml +0 -41
  62. nyl-0.10.4/docs/pyproject.toml +0 -11
  63. nyl-0.10.4/docs/uv.lock +0 -593
  64. nyl-0.10.4/example/manifests/postgres.yaml +0 -20
  65. nyl-0.10.4/example/nyl-profiles.toml +0 -10
  66. nyl-0.10.4/example/nyl-profiles.yaml +0 -10
  67. nyl-0.10.4/example/nyl-secrets.yaml +0 -3
  68. nyl-0.10.4/example/secrets.yaml +0 -26
  69. nyl-0.10.4/flake.lock +0 -348
  70. nyl-0.10.4/flake.nix +0 -113
  71. nyl-0.10.4/nix/uv.nix +0 -41
  72. nyl-0.10.4/pyproject.toml +0 -66
  73. nyl-0.10.4/renovate.json +0 -34
  74. nyl-0.10.4/src/kubernetes/README.md +0 -2
  75. nyl-0.10.4/src/kubernetes/dynamic/__init__.pyi +0 -3
  76. nyl-0.10.4/src/kubernetes/dynamic/client.pyi +0 -71
  77. nyl-0.10.4/src/kubernetes/dynamic/discovery.pyi +0 -30
  78. nyl-0.10.4/src/kubernetes/dynamic/exceptions.pyi +0 -47
  79. nyl-0.10.4/src/kubernetes/dynamic/resource.pyi +0 -43
  80. nyl-0.10.4/src/kubernetes/watch/__init__.pyi +0 -3
  81. nyl-0.10.4/src/kubernetes/watch/watch.pyi +0 -10
  82. nyl-0.10.4/src/nyl/__init__.py +0 -3
  83. nyl-0.10.4/src/nyl/commands/__init__.py +0 -159
  84. nyl-0.10.4/src/nyl/commands/add.py +0 -118
  85. nyl-0.10.4/src/nyl/commands/crds.py +0 -18
  86. nyl-0.10.4/src/nyl/commands/new.py +0 -134
  87. nyl-0.10.4/src/nyl/commands/profile.py +0 -40
  88. nyl-0.10.4/src/nyl/commands/run.py +0 -98
  89. nyl-0.10.4/src/nyl/commands/secrets.py +0 -93
  90. nyl-0.10.4/src/nyl/commands/template.py +0 -575
  91. nyl-0.10.4/src/nyl/commands/tools/__init__.py +0 -14
  92. nyl-0.10.4/src/nyl/commands/tools/bcrypt.py +0 -18
  93. nyl-0.10.4/src/nyl/commands/tools/sops.py +0 -99
  94. nyl-0.10.4/src/nyl/commands/tun.py +0 -126
  95. nyl-0.10.4/src/nyl/daemon.py +0 -344
  96. nyl-0.10.4/src/nyl/generator/__init__.py +0 -90
  97. nyl-0.10.4/src/nyl/generator/components.py +0 -92
  98. nyl-0.10.4/src/nyl/generator/dispatch.py +0 -109
  99. nyl-0.10.4/src/nyl/generator/dispatch_test.py +0 -56
  100. nyl-0.10.4/src/nyl/generator/helmchart.py +0 -221
  101. nyl-0.10.4/src/nyl/generator/helmchart_test.py +0 -44
  102. nyl-0.10.4/src/nyl/generator/statefulsecret.py +0 -27
  103. nyl-0.10.4/src/nyl/profiles/__init__.py +0 -175
  104. nyl-0.10.4/src/nyl/profiles/config.py +0 -182
  105. nyl-0.10.4/src/nyl/profiles/config_test.py +0 -60
  106. nyl-0.10.4/src/nyl/profiles/kubeconfig.py +0 -163
  107. nyl-0.10.4/src/nyl/profiles/tunnel.py +0 -248
  108. nyl-0.10.4/src/nyl/project/config.py +0 -217
  109. nyl-0.10.4/src/nyl/resources/__init__.py +0 -126
  110. nyl-0.10.4/src/nyl/resources/applyset.py +0 -239
  111. nyl-0.10.4/src/nyl/resources/applyset_test.py +0 -39
  112. nyl-0.10.4/src/nyl/resources/helmchart.py +0 -91
  113. nyl-0.10.4/src/nyl/resources/helmchart_test.py +0 -81
  114. nyl-0.10.4/src/nyl/resources/placeholder.py +0 -80
  115. nyl-0.10.4/src/nyl/resources/postprocessor.py +0 -206
  116. nyl-0.10.4/src/nyl/resources/postprocessor_test.py +0 -150
  117. nyl-0.10.4/src/nyl/resources/statefulsecret.py +0 -14
  118. nyl-0.10.4/src/nyl/secrets/__init__.py +0 -77
  119. nyl-0.10.4/src/nyl/secrets/config.py +0 -72
  120. nyl-0.10.4/src/nyl/secrets/config_test.py +0 -60
  121. nyl-0.10.4/src/nyl/secrets/kubernetes.py +0 -84
  122. nyl-0.10.4/src/nyl/secrets/null.py +0 -22
  123. nyl-0.10.4/src/nyl/secrets/sops.py +0 -160
  124. nyl-0.10.4/src/nyl/secrets/sops_test.py +0 -72
  125. nyl-0.10.4/src/nyl/templating.py +0 -244
  126. nyl-0.10.4/src/nyl/templating_test.py +0 -9
  127. nyl-0.10.4/src/nyl/tools/di.py +0 -84
  128. nyl-0.10.4/src/nyl/tools/fs.py +0 -67
  129. nyl-0.10.4/src/nyl/tools/fs_test.py +0 -17
  130. nyl-0.10.4/src/nyl/tools/kubectl.py +0 -179
  131. nyl-0.10.4/src/nyl/tools/kubernetes.py +0 -83
  132. nyl-0.10.4/src/nyl/tools/kvstore.py +0 -132
  133. nyl-0.10.4/src/nyl/tools/loads.py +0 -39
  134. nyl-0.10.4/src/nyl/tools/logging.py +0 -17
  135. nyl-0.10.4/src/nyl/tools/pyroscope.py +0 -58
  136. nyl-0.10.4/src/nyl/tools/shell.py +0 -6
  137. nyl-0.10.4/src/nyl/tools/testing.py +0 -12
  138. nyl-0.10.4/src/nyl/tools/typer.py +0 -9
  139. nyl-0.10.4/src/nyl/tools/types.py +0 -12
  140. nyl-0.10.4/src/nyl/tools/url.py +0 -33
  141. nyl-0.10.4/src/nyl/tools/yaml.py +0 -28
  142. nyl-0.10.4/src/pyroscope/__init__.pyi +0 -11
  143. nyl-0.10.4/testdata/helm-test-chart/Chart.yaml +0 -3
  144. nyl-0.10.4/testdata/helm-test-chart/templates/sa.yaml +0 -7
  145. nyl-0.10.4/testdata/helm-test-chart/values.yaml +0 -1
  146. nyl-0.10.4/tests/__init__.py +0 -0
  147. nyl-0.10.4/tests/conftest.py +0 -6
  148. nyl-0.10.4/uv.lock +0 -857
  149. /nyl-0.10.4/src/kubernetes/__init__.pyi → /nyl-0.26.0/src/nyl/py.typed +0 -0
nyl-0.26.0/.gitignore ADDED
@@ -0,0 +1,30 @@
1
+ # Nyl runtime cache
2
+ .nyl/
3
+
4
+ # Build artifacts
5
+ /result
6
+
7
+ # Rust
8
+ nyl/target/
9
+ nyl/Cargo.lock
10
+ nyl/book/book/
11
+ **/*.rs.bk
12
+ *.pdb
13
+
14
+ # Development
15
+ todo.txt
16
+ .claude/
17
+ .worktrees/
18
+
19
+ # Added by cargo
20
+
21
+ /target
22
+ .codex
23
+
24
+ # Python client
25
+ python/.venv/
26
+ python/dist/
27
+ __pycache__/
28
+ .pytest_cache/
29
+ .mypy_cache/
30
+ .ruff_cache/
nyl-0.26.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.5
2
+ Name: nyl
3
+ Version: 0.26.0
4
+ Summary: Render and inspect Kubernetes manifests with the Nyl CLI
5
+ Project-URL: Homepage, https://niklasrosenstein.github.io/nyl/
6
+ Project-URL: Repository, https://github.com/NiklasRosenstein/nyl
7
+ Author: Niklas Rosenstein
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: ruamel-yaml<0.19,>=0.18.6
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Nyl Python client
14
+
15
+ `nyl` is a small Python 3.11+ library for rendering and inspecting Kubernetes
16
+ manifests. Install it with `uv add nyl` or `pip install nyl`.
17
+
18
+ Install the [Nyl CLI](https://niklasrosenstein.github.io/nyl/start/getting-started/)
19
+ separately and put it on `PATH`, or pass its executable path.
20
+
21
+ ```python
22
+ from nyl import NylClient
23
+
24
+ client = NylClient(project="/path/to/infrastructure", executable="nyl", timeout=120)
25
+ manifests = client.render("applications/workloads/web.yaml", target="development")
26
+ deployment = manifests.one("Deployment", "web")
27
+
28
+ assert deployment.namespace == "web"
29
+ for container in deployment.containers(include_init=True):
30
+ assert container.security_context["allowPrivilegeEscalation"] is False
31
+ assert container.env.value("PGHOST") == "database-rw"
32
+ assert container.env.secret_ref("PGPASSWORD") == ("database-app", "password")
33
+ ```
34
+
35
+ `namespace` is the authored namespace. Resources without one return `None`,
36
+ even when the Release or Argo CD Application has a destination namespace.
37
+
38
+ ## Rendering
39
+
40
+ `NylClient(project, executable="nyl", timeout=120)` fixes the invocation directory
41
+ and requires a positive, finite timeout in seconds. `render(source, *, target=None,
42
+ offline=False, allow_secret_inputs=False)` invokes only `nyl render`. Relative
43
+ source paths are relative to `project`; absolute paths are passed through.
44
+ Calls leave Python's working directory and environment unchanged and use no shell.
45
+ Timeouts terminate the invocation's process group on POSIX and its process tree
46
+ with Windows' built-in `taskkill`; diagnostics are collected after termination.
47
+
48
+ Rust owns configuration resolution, validation, rendering, and caching. Target-aware
49
+ Releases use the compiled target's rendering rules. Each call runs the CLI again;
50
+ Nyl's cache may reuse valid artifacts. Project-configured validation remains enabled
51
+ according to the CLI's defaults. A validation failure yields an exception even if
52
+ the CLI emits manifests. `offline=True` forwards `--offline` (cached Git references
53
+ and publication inputs); it is not a universal network prohibition for validators
54
+ or other external tools. `allow_secret_inputs=True` explicitly forwards the CLI's
55
+ opt-in for project secrets and admitted environment inputs.
56
+
57
+ Only single-file render entry points are supported. Full-tree loading needs a
58
+ documented manifest inventory: a published tree can also contain ownership indexes
59
+ and non-manifest state. This package neither downloads the executable nor applies,
60
+ publishes, or reconciles resources, and has no persistent Python cache.
61
+
62
+ ## Inspecting declarations
63
+
64
+ `ManifestSet` is iterable, has a length, and preserves document order.
65
+ `select(kind=None, name=None, *, namespace=..., api_version=None)` returns another
66
+ `ManifestSet`. `one(kind, name=None, *, namespace=..., api_version=None)` requires
67
+ exactly one match. Omit `namespace` to search all namespaces; explicitly pass
68
+ `namespace=None` to select resources with no declared namespace. Selection errors
69
+ show resource identities without manifest bodies. `ManifestSet.from_yaml(text)`
70
+ safely reads multi-document YAML 1.2, ignores empty documents, and rejects invalid
71
+ resource envelopes. Nyl's emitted quoting preserves strings such as `"on"`, `"yes"`,
72
+ and `"001"`; no YAML 1.1 boolean coercion is applied by Python.
73
+
74
+ Resources expose `raw`, `api_version`, `kind`, `name`, and `namespace`.
75
+ `containers(include_init=False)` handles Pods, Deployments, StatefulSets, DaemonSets,
76
+ ReplicaSets, ReplicationControllers, Jobs, and CronJobs. It yields regular containers
77
+ in order, followed by init containers when requested. Unsupported kinds raise
78
+ `DeclarationError`.
79
+
80
+ Containers expose `raw`, `name`, `security_context` (an empty dictionary when
81
+ omitted), and `env`. `env.value(name)` requires an explicit literal string;
82
+ `env.secret_ref(name)` requires one Secret key reference and returns `(name, key)`.
83
+ Missing, duplicate, malformed, and incompatible declarations raise `DeclarationError`.
84
+ These helpers do not resolve Secrets, ConfigMaps, `envFrom`, Pod-level security
85
+ context inheritance, or live cluster data. Raw dictionaries remain mutable.
86
+
87
+ ## Errors and compatibility
88
+
89
+ All library errors derive from `NylError` and are exported from `nyl`:
90
+
91
+ | Error | Meaning |
92
+ | --- | --- |
93
+ | `ExecutableNotFoundError` | Install the CLI or correct its executable path |
94
+ | `RenderTimeoutError` | The subprocess exceeded its deadline |
95
+ | `RenderError` | A nonzero exit status or an OS execution error |
96
+ | `InvalidOutputError` | stdout is not valid rendered resource YAML |
97
+ | `ResourceNotFoundError` | `one()` found zero matches |
98
+ | `AmbiguousResourceError` | `one()` found several matches |
99
+ | `DeclarationError` | A helper cannot interpret the declaration |
100
+
101
+ Execution errors derive from `ExecutionError` and retain `command` (an argument
102
+ tuple), `exit_code` (`None` when unavailable), and `diagnostics` (stderr).
103
+ They do not retain or automatically print stdout. YAML errors omit input excerpts;
104
+ resource and container representations include only identities. CLI diagnostics
105
+ can contain sensitive information and should be handled accordingly.
106
+
107
+ Tested with Nyl CLI **0.5.9**. The package's version is available through
108
+ `importlib.metadata.version("nyl")`.
109
+
110
+ ## Development and releases
111
+
112
+ From the repository root:
113
+
114
+ ```bash
115
+ mise run python-check # Ruff, type checking, parallel tests, checkout-built CLI
116
+ mise run python-build # Stamp Cargo-derived version, then build wheel and sdist
117
+ mise run pre-commit # Required Rust and Python checks
118
+ ```
119
+
120
+ Or inside `python/`:
121
+
122
+ ```bash
123
+ uv sync --locked
124
+ uv run python scripts/version.py
125
+ uv run ruff check .
126
+ uv run ruff format --check .
127
+ uv run mypy
128
+ uv run pytest
129
+ uv build
130
+ ```
131
+
132
+ The test fixture builds the CLI and reads Cargo's executable artifact, honoring
133
+ Cargo's configured target and output directory. Set `NYL_TEST_EXECUTABLE` to use
134
+ a binary you built separately.
135
+
136
+ `scripts/version.py` derives the release version from root `Cargo.toml` and stamps
137
+ `src/nyl/_version.py` before building. For CLI `0.MINOR.PATCH`, Python uses
138
+ `0.2MINOR.PATCH` (concatenation): `0.5.9` → `0.25.9`, `0.10.3` → `0.210.3`.
139
+ From CLI 1.x, versions match. Strict SemVer prereleases `alpha.N`, `beta.N`, and
140
+ `rc.N` map to PEP 440 `aN`, `bN`, and `rcN`. Other prereleases and build metadata
141
+ are rejected. The stamped file is build metadata, not a separately maintained
142
+ release counter. Stamp after synchronizing the development environment;
143
+ `uv run --no-sync` can use that environment when preparing a newer native release.
144
+ The source distribution includes its stamped version and builds without the Rust
145
+ repository, Cargo, or Nyl.
146
+
147
+ The handwritten `.github/workflows/python.yaml` tests and builds artifacts on
148
+ relevant pull requests, `develop` changes, and native `v*` tags. The tag must match
149
+ the Cargo version. Its publish job consumes the tested artifacts and uses PyPI
150
+ Trusted Publishing from the `pypi` environment for `NiklasRosenstein/nyl`.
151
+ Release through the repository's native release command (`scripts/release.sh X.Y.Z`);
152
+ no separate Python counter or PyPI API token is needed.
nyl-0.26.0/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # Nyl Python client
2
+
3
+ `nyl` is a small Python 3.11+ library for rendering and inspecting Kubernetes
4
+ manifests. Install it with `uv add nyl` or `pip install nyl`.
5
+
6
+ Install the [Nyl CLI](https://niklasrosenstein.github.io/nyl/start/getting-started/)
7
+ separately and put it on `PATH`, or pass its executable path.
8
+
9
+ ```python
10
+ from nyl import NylClient
11
+
12
+ client = NylClient(project="/path/to/infrastructure", executable="nyl", timeout=120)
13
+ manifests = client.render("applications/workloads/web.yaml", target="development")
14
+ deployment = manifests.one("Deployment", "web")
15
+
16
+ assert deployment.namespace == "web"
17
+ for container in deployment.containers(include_init=True):
18
+ assert container.security_context["allowPrivilegeEscalation"] is False
19
+ assert container.env.value("PGHOST") == "database-rw"
20
+ assert container.env.secret_ref("PGPASSWORD") == ("database-app", "password")
21
+ ```
22
+
23
+ `namespace` is the authored namespace. Resources without one return `None`,
24
+ even when the Release or Argo CD Application has a destination namespace.
25
+
26
+ ## Rendering
27
+
28
+ `NylClient(project, executable="nyl", timeout=120)` fixes the invocation directory
29
+ and requires a positive, finite timeout in seconds. `render(source, *, target=None,
30
+ offline=False, allow_secret_inputs=False)` invokes only `nyl render`. Relative
31
+ source paths are relative to `project`; absolute paths are passed through.
32
+ Calls leave Python's working directory and environment unchanged and use no shell.
33
+ Timeouts terminate the invocation's process group on POSIX and its process tree
34
+ with Windows' built-in `taskkill`; diagnostics are collected after termination.
35
+
36
+ Rust owns configuration resolution, validation, rendering, and caching. Target-aware
37
+ Releases use the compiled target's rendering rules. Each call runs the CLI again;
38
+ Nyl's cache may reuse valid artifacts. Project-configured validation remains enabled
39
+ according to the CLI's defaults. A validation failure yields an exception even if
40
+ the CLI emits manifests. `offline=True` forwards `--offline` (cached Git references
41
+ and publication inputs); it is not a universal network prohibition for validators
42
+ or other external tools. `allow_secret_inputs=True` explicitly forwards the CLI's
43
+ opt-in for project secrets and admitted environment inputs.
44
+
45
+ Only single-file render entry points are supported. Full-tree loading needs a
46
+ documented manifest inventory: a published tree can also contain ownership indexes
47
+ and non-manifest state. This package neither downloads the executable nor applies,
48
+ publishes, or reconciles resources, and has no persistent Python cache.
49
+
50
+ ## Inspecting declarations
51
+
52
+ `ManifestSet` is iterable, has a length, and preserves document order.
53
+ `select(kind=None, name=None, *, namespace=..., api_version=None)` returns another
54
+ `ManifestSet`. `one(kind, name=None, *, namespace=..., api_version=None)` requires
55
+ exactly one match. Omit `namespace` to search all namespaces; explicitly pass
56
+ `namespace=None` to select resources with no declared namespace. Selection errors
57
+ show resource identities without manifest bodies. `ManifestSet.from_yaml(text)`
58
+ safely reads multi-document YAML 1.2, ignores empty documents, and rejects invalid
59
+ resource envelopes. Nyl's emitted quoting preserves strings such as `"on"`, `"yes"`,
60
+ and `"001"`; no YAML 1.1 boolean coercion is applied by Python.
61
+
62
+ Resources expose `raw`, `api_version`, `kind`, `name`, and `namespace`.
63
+ `containers(include_init=False)` handles Pods, Deployments, StatefulSets, DaemonSets,
64
+ ReplicaSets, ReplicationControllers, Jobs, and CronJobs. It yields regular containers
65
+ in order, followed by init containers when requested. Unsupported kinds raise
66
+ `DeclarationError`.
67
+
68
+ Containers expose `raw`, `name`, `security_context` (an empty dictionary when
69
+ omitted), and `env`. `env.value(name)` requires an explicit literal string;
70
+ `env.secret_ref(name)` requires one Secret key reference and returns `(name, key)`.
71
+ Missing, duplicate, malformed, and incompatible declarations raise `DeclarationError`.
72
+ These helpers do not resolve Secrets, ConfigMaps, `envFrom`, Pod-level security
73
+ context inheritance, or live cluster data. Raw dictionaries remain mutable.
74
+
75
+ ## Errors and compatibility
76
+
77
+ All library errors derive from `NylError` and are exported from `nyl`:
78
+
79
+ | Error | Meaning |
80
+ | --- | --- |
81
+ | `ExecutableNotFoundError` | Install the CLI or correct its executable path |
82
+ | `RenderTimeoutError` | The subprocess exceeded its deadline |
83
+ | `RenderError` | A nonzero exit status or an OS execution error |
84
+ | `InvalidOutputError` | stdout is not valid rendered resource YAML |
85
+ | `ResourceNotFoundError` | `one()` found zero matches |
86
+ | `AmbiguousResourceError` | `one()` found several matches |
87
+ | `DeclarationError` | A helper cannot interpret the declaration |
88
+
89
+ Execution errors derive from `ExecutionError` and retain `command` (an argument
90
+ tuple), `exit_code` (`None` when unavailable), and `diagnostics` (stderr).
91
+ They do not retain or automatically print stdout. YAML errors omit input excerpts;
92
+ resource and container representations include only identities. CLI diagnostics
93
+ can contain sensitive information and should be handled accordingly.
94
+
95
+ Tested with Nyl CLI **0.5.9**. The package's version is available through
96
+ `importlib.metadata.version("nyl")`.
97
+
98
+ ## Development and releases
99
+
100
+ From the repository root:
101
+
102
+ ```bash
103
+ mise run python-check # Ruff, type checking, parallel tests, checkout-built CLI
104
+ mise run python-build # Stamp Cargo-derived version, then build wheel and sdist
105
+ mise run pre-commit # Required Rust and Python checks
106
+ ```
107
+
108
+ Or inside `python/`:
109
+
110
+ ```bash
111
+ uv sync --locked
112
+ uv run python scripts/version.py
113
+ uv run ruff check .
114
+ uv run ruff format --check .
115
+ uv run mypy
116
+ uv run pytest
117
+ uv build
118
+ ```
119
+
120
+ The test fixture builds the CLI and reads Cargo's executable artifact, honoring
121
+ Cargo's configured target and output directory. Set `NYL_TEST_EXECUTABLE` to use
122
+ a binary you built separately.
123
+
124
+ `scripts/version.py` derives the release version from root `Cargo.toml` and stamps
125
+ `src/nyl/_version.py` before building. For CLI `0.MINOR.PATCH`, Python uses
126
+ `0.2MINOR.PATCH` (concatenation): `0.5.9` → `0.25.9`, `0.10.3` → `0.210.3`.
127
+ From CLI 1.x, versions match. Strict SemVer prereleases `alpha.N`, `beta.N`, and
128
+ `rc.N` map to PEP 440 `aN`, `bN`, and `rcN`. Other prereleases and build metadata
129
+ are rejected. The stamped file is build metadata, not a separately maintained
130
+ release counter. Stamp after synchronizing the development environment;
131
+ `uv run --no-sync` can use that environment when preparing a newer native release.
132
+ The source distribution includes its stamped version and builds without the Rust
133
+ repository, Cargo, or Nyl.
134
+
135
+ The handwritten `.github/workflows/python.yaml` tests and builds artifacts on
136
+ relevant pull requests, `develop` changes, and native `v*` tags. The tag must match
137
+ the Cargo version. Its publish job consumes the tested artifacts and uses PyPI
138
+ Trusted Publishing from the `pypi` environment for `NiklasRosenstein/nyl`.
139
+ Release through the repository's native release command (`scripts/release.sh X.Y.Z`);
140
+ no separate Python counter or PyPI API token is needed.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<2"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nyl"
7
+ dynamic = ["version"]
8
+ description = "Render and inspect Kubernetes manifests with the Nyl CLI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{name = "Niklas Rosenstein"}]
13
+ dependencies = ["ruamel.yaml>=0.18.6,<0.19"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://niklasrosenstein.github.io/nyl/"
17
+ Repository = "https://github.com/NiklasRosenstein/nyl"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "build>=1.2,<2",
22
+ "hatchling>=1.27,<2",
23
+ "mypy>=1.15,<2",
24
+ "pip>=25",
25
+ "pytest>=8,<10",
26
+ "pytest-xdist>=3.6,<4",
27
+ "ruff>=0.11,<1",
28
+ ]
29
+
30
+ [tool.hatch.version]
31
+ path = "src/nyl/_version.py"
32
+
33
+ [tool.hatch.build.targets.sdist]
34
+ include = ["src/nyl", "README.md", "pyproject.toml"]
35
+
36
+ [tool.ruff]
37
+ target-version = "py311"
38
+ line-length = 100
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E", "F", "I", "UP", "B"]
42
+
43
+ [tool.mypy]
44
+ python_version = "3.11"
45
+ strict = true
46
+ files = ["src/nyl", "scripts/version.py"]
47
+
48
+ [tool.pytest.ini_options]
49
+ addopts = "-n auto --maxprocesses=4"
50
+ testpaths = ["tests"]
@@ -0,0 +1,34 @@
1
+ """Render Kubernetes manifests through an independently installed Nyl CLI."""
2
+
3
+ from .client import NylClient
4
+ from .errors import (
5
+ AmbiguousResourceError,
6
+ DeclarationError,
7
+ ExecutableNotFoundError,
8
+ ExecutionError,
9
+ InvalidOutputError,
10
+ NylError,
11
+ RenderError,
12
+ RenderTimeoutError,
13
+ ResourceNotFoundError,
14
+ SelectionError,
15
+ )
16
+ from .manifests import Container, Environment, ManifestSet, Resource
17
+
18
+ __all__ = [
19
+ "AmbiguousResourceError",
20
+ "Container",
21
+ "DeclarationError",
22
+ "Environment",
23
+ "ExecutableNotFoundError",
24
+ "ExecutionError",
25
+ "InvalidOutputError",
26
+ "ManifestSet",
27
+ "NylClient",
28
+ "NylError",
29
+ "RenderError",
30
+ "RenderTimeoutError",
31
+ "Resource",
32
+ "ResourceNotFoundError",
33
+ "SelectionError",
34
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.26.0"
@@ -0,0 +1,113 @@
1
+ """The subprocess boundary delegates rendering policy and caching to Rust."""
2
+
3
+ import math
4
+ import os
5
+ import signal
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .errors import ExecutableNotFoundError, RenderError, RenderTimeoutError
11
+ from .manifests import ManifestSet
12
+
13
+
14
+ class NylClient:
15
+ """Invoke an installed Nyl executable from a fixed project directory."""
16
+
17
+ def __init__(
18
+ self,
19
+ project: str | os.PathLike[str],
20
+ executable: str | os.PathLike[str] = "nyl",
21
+ timeout: float = 120,
22
+ ) -> None:
23
+ if not math.isfinite(timeout) or timeout <= 0:
24
+ raise ValueError("timeout must be positive and finite")
25
+ self.project = Path(project).resolve()
26
+ self.executable = os.fspath(executable)
27
+ self.timeout = timeout
28
+
29
+ def render(
30
+ self,
31
+ source: str | os.PathLike[str],
32
+ *,
33
+ target: str | None = None,
34
+ offline: bool = False,
35
+ allow_secret_inputs: bool = False,
36
+ ) -> ManifestSet:
37
+ """Render one source file, preserving the project's validation defaults.
38
+
39
+ Relative source paths are resolved by the CLI from ``project``. Each call
40
+ executes the CLI afresh; its own cache decides which artifacts to reuse.
41
+ """
42
+ command = [self.executable, "render"]
43
+ if target is not None:
44
+ command.extend(["--target", target])
45
+ if offline:
46
+ command.append("--offline")
47
+ if allow_secret_inputs:
48
+ command.append("--allow-secret-inputs")
49
+ command.extend(["--", os.fspath(source)])
50
+ try:
51
+ with subprocess.Popen(
52
+ command,
53
+ shell=False,
54
+ cwd=self.project,
55
+ stdout=subprocess.PIPE,
56
+ stderr=subprocess.PIPE,
57
+ start_new_session=sys.platform != "win32",
58
+ ) as process:
59
+ try:
60
+ stdout, stderr = process.communicate(timeout=self.timeout)
61
+ except BaseException as error:
62
+ _terminate_tree(process)
63
+ _, stderr = process.communicate(timeout=5)
64
+ if isinstance(error, subprocess.TimeoutExpired):
65
+ error.stderr = stderr
66
+ raise
67
+ exit_code = process.returncode
68
+ except FileNotFoundError:
69
+ if not self.project.is_dir():
70
+ raise RenderError(
71
+ f"Project directory does not exist: {self.project}", command=command
72
+ ) from None
73
+ raise ExecutableNotFoundError(
74
+ f"Nyl executable not found: {self.executable!r}; install the Nyl CLI separately",
75
+ command=command,
76
+ ) from None
77
+ except subprocess.TimeoutExpired as error:
78
+ raise RenderTimeoutError(
79
+ f"Nyl rendering exceeded {self.timeout} seconds",
80
+ command=command,
81
+ diagnostics=(error.stderr or b"").decode("utf-8", errors="replace"),
82
+ ) from None
83
+ except OSError as error:
84
+ raise RenderError(f"Could not execute Nyl: {error.strerror}", command=command) from None
85
+ diagnostics = stderr.decode("utf-8", errors="replace")
86
+ if exit_code:
87
+ raise RenderError(
88
+ f"Nyl rendering failed with exit code {exit_code}",
89
+ command=command,
90
+ diagnostics=diagnostics,
91
+ exit_code=exit_code,
92
+ )
93
+ return ManifestSet.from_yaml(stdout)
94
+
95
+
96
+ def _terminate_tree(process: subprocess.Popen[bytes]) -> None:
97
+ if sys.platform == "win32":
98
+ try:
99
+ subprocess.run(
100
+ ["taskkill", "/F", "/T", "/PID", str(process.pid)],
101
+ stdout=subprocess.DEVNULL,
102
+ stderr=subprocess.DEVNULL,
103
+ timeout=5,
104
+ check=False,
105
+ )
106
+ finally:
107
+ if process.poll() is None:
108
+ process.kill()
109
+ else:
110
+ try:
111
+ os.killpg(process.pid, signal.SIGKILL)
112
+ except ProcessLookupError:
113
+ pass
@@ -0,0 +1,54 @@
1
+ """Errors contain diagnostics and identities, never rendered manifest bodies."""
2
+
3
+
4
+ class NylError(Exception):
5
+ """Base class for the client's errors."""
6
+
7
+
8
+ class ExecutionError(NylError):
9
+ """A CLI invocation failed; diagnostics may contain sensitive CLI messages."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ *,
15
+ command: list[str],
16
+ diagnostics: str = "",
17
+ exit_code: int | None = None,
18
+ ) -> None:
19
+ self.command = tuple(command)
20
+ self.diagnostics = diagnostics
21
+ self.exit_code = exit_code
22
+ super().__init__(f"{message}\n{diagnostics}".rstrip())
23
+
24
+
25
+ class ExecutableNotFoundError(ExecutionError):
26
+ """The configured Nyl executable is unavailable."""
27
+
28
+
29
+ class RenderTimeoutError(ExecutionError):
30
+ """Rendering exceeded the client's timeout."""
31
+
32
+
33
+ class RenderError(ExecutionError):
34
+ """The CLI returned a nonzero exit code or could not start."""
35
+
36
+
37
+ class InvalidOutputError(NylError):
38
+ """Rendered stdout is not a stream of Kubernetes resource dictionaries."""
39
+
40
+
41
+ class SelectionError(NylError):
42
+ """A resource selector does not identify exactly one resource."""
43
+
44
+
45
+ class ResourceNotFoundError(SelectionError):
46
+ """No resource matches a selector."""
47
+
48
+
49
+ class AmbiguousResourceError(SelectionError):
50
+ """Several resources match a selector."""
51
+
52
+
53
+ class DeclarationError(NylError):
54
+ """A container helper cannot interpret the authored declaration."""