docker-dsl 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yasyf Mohamedali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: docker-dsl
3
+ Version: 0.1.0
4
+ Summary: Imperative context-manager DSL for authoring multi-stage Dockerfiles.
5
+ Keywords:
6
+ Author: Yasyf Mohamedali
7
+ Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: pydantic>=2.0
17
+ Requires-Dist: executing>=2.0
18
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
19
+ Requires-Dist: anyio>=4 ; extra == 'dev'
20
+ Requires-Dist: inline-snapshot>=0.17 ; extra == 'dev'
21
+ Requires-Dist: ruff>=0.8 ; extra == 'dev'
22
+ Requires-Dist: ty>=0.0.44 ; extra == 'dev'
23
+ Requires-Python: >=3.13
24
+ Project-URL: Homepage, https://github.com/yasyf/docker-dsl
25
+ Project-URL: Documentation, https://yasyf.github.io/docker-dsl/
26
+ Project-URL: Repository, https://github.com/yasyf/docker-dsl
27
+ Project-URL: Issues, https://github.com/yasyf/docker-dsl/issues
28
+ Project-URL: Changelog, https://github.com/yasyf/docker-dsl/blob/main/CHANGELOG.md
29
+ Provides-Extra: dev
30
+ Description-Content-Type: text/markdown
31
+
32
+ # docker-dsl
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/docker-dsl.svg)](https://pypi.org/project/docker-dsl/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/docker-dsl.svg)](https://pypi.org/project/docker-dsl/)
36
+ [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/docker-dsl/docs.yml?branch=main&label=docs)](https://yasyf.github.io/docker-dsl/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/yasyf/docker-dsl/blob/main/LICENSE)
38
+
39
+ An imperative, context-manager-based Python DSL for authoring multi-stage
40
+ Dockerfiles. Write builds the way you write Python — `with` blocks,
41
+ conditionals, comprehensions, and reusable helpers — and render one recipe
42
+ into many Dockerfile variants.
43
+
44
+ ## Install
45
+
46
+ No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
47
+
48
+ ```bash
49
+ uvx docker-dsl --help
50
+ ```
51
+
52
+ `uvx` fetches docker-dsl into a throwaway environment and runs it. To add it
53
+ to a project instead:
54
+
55
+ ```bash
56
+ uv add docker-dsl
57
+ ```
58
+
59
+ ## Quickstart
60
+
61
+ Write a recipe module — for example `my_recipe.py`:
62
+
63
+ ```python
64
+ from docker_dsl import Stage, context as ctx
65
+
66
+ ctx.register("gpu", bool)
67
+
68
+ with Stage("nvcr.io/nvidia/pytorch:26.03-py3" if ctx.gpu else "ubuntu:24.04") as s:
69
+ s.arg("PYTHON_VERSION", "3.13", env=True)
70
+ s.path("/root/.local/bin")
71
+ s.workdir("/root")
72
+
73
+ with s.cache("/var/cache/apt", lock=True), s.cache("/var/lib/apt", lock=True):
74
+ s.apt_install("software-properties-common")
75
+ s.add_apt_ppa("ppa:apt-fast/stable")
76
+ s.apt_install("curl", "git", "wget", fast=True)
77
+
78
+ with s.cache("/root/.cache/uv"), s.run() as r:
79
+ r.uv("pip", "install", "-U", "numpy", "pandas")
80
+ ```
81
+
82
+ Render it:
83
+
84
+ ```bash
85
+ docker-dsl my_recipe --gpu=true --out Dockerfile
86
+ ```
87
+
88
+ Or from Python:
89
+
90
+ ```python
91
+ from docker_dsl import Dockerfile
92
+ import my_recipe
93
+
94
+ with Dockerfile(my_recipe) as d:
95
+ gpu_text = d.render(gpu=True)
96
+ cpu_text = d.render(gpu=False)
97
+ ```
98
+
99
+ ## What problems does this solve?
100
+
101
+ - **`RUN` chains glued with `&&` are fragile.** The run builder accumulates
102
+ commands in Python — `r.git("clone", url)`, `r.make("-j$(nproc)")` — and
103
+ emits one correct `RUN` instruction, with `cd` scoping restored
104
+ automatically.
105
+ - **Variants drift apart.** A GPU and a CPU image usually means two copied
106
+ Dockerfiles diverging over time. Here one recipe takes `--gpu=true|false`
107
+ and renders both from the same code path.
108
+ - **No abstraction in raw Dockerfiles.** Recipes are plain Python modules:
109
+ factor repeated setup into context managers and helper functions, loop and
110
+ branch where the build genuinely varies.
111
+ - **BuildKit mounts are easy to get wrong.** `s.cache(...)`, `s.secret(...)`,
112
+ and `s.bind(...)` are scoped context managers — every `RUN` inside the
113
+ scope picks up the active mounts, and nothing leaks outside it.
114
+
115
+ ## Core concepts
116
+
117
+ ### Two-pass execution
118
+
119
+ When a recipe is first imported, docker-dsl is in **discovery pass**: the
120
+ module body runs but every DSL call is a no-op. `ctx.register(...)` is used
121
+ in this pass to populate a schema of config fields.
122
+
123
+ When `Dockerfile(module).render(**config)` is called, the DSL enters the
124
+ **render pass**: it validates `config` against the registered schema, sets
125
+ up ContextVars, and re-executes the module. Every `with Stage(...) as s:`
126
+ and `s.<method>(...)` now accumulates into the active graph. `ctx.gpu`
127
+ returns the validated config value.
128
+
129
+ This design lets you write the recipe as plain top-level Python — no magic,
130
+ no decorators, no entrypoint functions.
131
+
132
+ ### Config fields
133
+
134
+ ```python
135
+ ctx.register("gpu", bool)
136
+ ctx.register("tag", str)
137
+ ```
138
+
139
+ Every registered field is required at render time. Pydantic validates the
140
+ values before the render pass runs, so type errors surface with clear
141
+ messages.
142
+
143
+ ### Stages
144
+
145
+ ```python
146
+ with Stage("ubuntu:24.04") as base: # FROM ubuntu:24.04 AS base
147
+ base.workdir("/root")
148
+
149
+ with base.stage() as builder: # FROM base AS builder
150
+ builder.run("make", "all")
151
+
152
+ with base.stage() as release: # FROM base AS release
153
+ release.copy("/out/bin", stage=builder)
154
+ ```
155
+
156
+ Child stage names are inferred from the `as <name>` target via the `executing`
157
+ library. Pass `name="..."` to override.
158
+
159
+ ### Run builder
160
+
161
+ `s.run()` as a context manager accumulates shell commands into a single
162
+ `RUN` instruction:
163
+
164
+ ```python
165
+ with s.run() as r:
166
+ r.git("clone", "https://example.com/repo.git")
167
+ with r.cd("repo"):
168
+ r.make("-j$(nproc)")
169
+ r.make("install")
170
+ r("(cd subdir && python setup.py install)") # raw fallback
171
+ ```
172
+
173
+ Command methods dispatch via `__getattr__` — any attribute name becomes the
174
+ shell binary. `r.cd(path)` works both as a statement (subsequent commands
175
+ stay in that directory) and as a context manager (scope-restores on exit via
176
+ `cd -`).
177
+
178
+ Echo redirects compose naturally:
179
+
180
+ ```python
181
+ with s.run() as r:
182
+ r.echo("pillow>=11.1.0") >> "/root/overrides.txt" # append
183
+ r.echo("numpy<3.0.0,>=1.26.4") > "/etc/pip/constraint.txt" # truncate
184
+ ```
185
+
186
+ ### Mounts
187
+
188
+ `s.cache(target, *, lock=False)`, `s.secret(id, *, target=...)`,
189
+ `s.bind(source=..., target=...)` return context managers that push a mount
190
+ onto the stage's stack. Any `RUN` emitted inside the scope picks up all
191
+ active mounts.
192
+
193
+ ```python
194
+ with s.cache("/root/.cache/uv"), s.secret("aws", target="/root/.aws/credentials"):
195
+ with s.run() as r:
196
+ r.uv("pip", "install", "-U", "torch")
197
+ ```
198
+
199
+ ### Smart apt
200
+
201
+ `r.apt_install(...)`, `r.add_apt_ppa(...)`, and `r.add_apt_repo(...)` are
202
+ methods on `RunBuilder` that write directly to the command list.
203
+ `apt-get update -y` is inserted automatically before the first install and
204
+ again after any dirty-marking operation (new PPA, new repo). Arbitrary
205
+ commands (cleanup, setup scripts) are just `r(...)` calls.
206
+
207
+ ```python
208
+ with s.cache("/var/cache/apt", lock=True), s.cache("/var/lib/apt", lock=True), s.run() as r:
209
+ r.apt_install("software-properties-common")
210
+ r.add_apt_ppa("ppa:apt-fast/stable")
211
+ r.apt_install("apt-fast", "curl", "wget", fast=True)
212
+ r("rm -rf /tmp/*")
213
+ ```
214
+
215
+ ### Reusable helpers
216
+
217
+ Recipes can compose their own context managers:
218
+
219
+ ```python
220
+ from contextlib import contextmanager
221
+
222
+ @contextmanager
223
+ def sccache(stage):
224
+ with stage.secret("aws", target="/root/.aws/credentials"):
225
+ yield
226
+
227
+ with Stage("ubuntu:24.04") as s:
228
+ with sccache(s), s.run() as r:
229
+ r.uv("pip", "install", "-U", "torch")
230
+ ```
231
+
232
+ ## CLI
233
+
234
+ ```
235
+ docker-dsl <module.path> [--<field>=<value> ...] [--out PATH]
236
+ ```
237
+
238
+ (`python -m docker_dsl` is equivalent.) Arguments are built dynamically from
239
+ the fields registered by the recipe. A `--out` argument is always available;
240
+ if omitted, the rendered Dockerfile is written to stdout. Bool fields accept
241
+ `true`/`false`/`1`/`0`/`yes`/`no`.
242
+
243
+ Validation errors surface with structured Pydantic messages naming the
244
+ missing or wrong-typed fields.
245
+
246
+ ## Examples
247
+
248
+ See [`examples/`](examples) for self-contained recipes:
249
+
250
+ - `minimal.py` — the shortest possible recipe
251
+ - `multi_stage.py` — builder + release pattern with `COPY --from`
252
+ - `apt_smart.py` — demonstrating the apt buffer flush rules
253
+
254
+ Run them via the CLI:
255
+
256
+ ```bash
257
+ docker-dsl examples.minimal --tag=dev --out Dockerfile.min
258
+ docker-dsl examples.multi_stage --release=true --out Dockerfile.ms
259
+ ```
260
+
261
+ ## Editor completions
262
+
263
+ `RunBuilder` uses `__getattr__` for dynamic shell-command dispatch, which
264
+ means editors have no static information about available commands. To fix
265
+ this, docker-dsl ships a generated `builder.pyi` type stub that declares
266
+ every system command as a method on `RunBuilder`.
267
+
268
+ Commands with bash programmable completions get `@overload` stubs with
269
+ `Literal` subcommands and typed flag kwargs. Commands without completions
270
+ get their flags extracted from man pages. All others get a plain
271
+ `*args: str, **kwargs: str | bool` catch-all.
272
+
273
+ Regenerate after installing new tools:
274
+
275
+ ```bash
276
+ python -m docker_dsl.stubgen
277
+ ```
278
+
279
+ The generator:
280
+ 1. Runs `bash -lic 'compgen -c'` to enumerate commands
281
+ 2. Invokes bash completion functions to extract subcommands + flags
282
+ 3. Falls back to `man <cmd> | col -b` (parallelized across CPU cores) for commands without bash completions
283
+ 4. Parses `builder.py` via `ast` to preserve real method signatures
284
+ 5. Writes `builder.pyi` with `@generated` header
285
+
286
+ Flags become keyword arguments with underscore-to-hyphen conversion:
287
+ `r.git("clone", url, depth="1", verbose=True)` → `git clone <url> --depth 1 --verbose`.
288
+
289
+ ## Docs
290
+
291
+ [Read the docs](https://yasyf.github.io/docker-dsl/) for the full guide and API reference.
@@ -0,0 +1,260 @@
1
+ # docker-dsl
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/docker-dsl.svg)](https://pypi.org/project/docker-dsl/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/docker-dsl.svg)](https://pypi.org/project/docker-dsl/)
5
+ [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/docker-dsl/docs.yml?branch=main&label=docs)](https://yasyf.github.io/docker-dsl/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/yasyf/docker-dsl/blob/main/LICENSE)
7
+
8
+ An imperative, context-manager-based Python DSL for authoring multi-stage
9
+ Dockerfiles. Write builds the way you write Python — `with` blocks,
10
+ conditionals, comprehensions, and reusable helpers — and render one recipe
11
+ into many Dockerfile variants.
12
+
13
+ ## Install
14
+
15
+ No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
16
+
17
+ ```bash
18
+ uvx docker-dsl --help
19
+ ```
20
+
21
+ `uvx` fetches docker-dsl into a throwaway environment and runs it. To add it
22
+ to a project instead:
23
+
24
+ ```bash
25
+ uv add docker-dsl
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ Write a recipe module — for example `my_recipe.py`:
31
+
32
+ ```python
33
+ from docker_dsl import Stage, context as ctx
34
+
35
+ ctx.register("gpu", bool)
36
+
37
+ with Stage("nvcr.io/nvidia/pytorch:26.03-py3" if ctx.gpu else "ubuntu:24.04") as s:
38
+ s.arg("PYTHON_VERSION", "3.13", env=True)
39
+ s.path("/root/.local/bin")
40
+ s.workdir("/root")
41
+
42
+ with s.cache("/var/cache/apt", lock=True), s.cache("/var/lib/apt", lock=True):
43
+ s.apt_install("software-properties-common")
44
+ s.add_apt_ppa("ppa:apt-fast/stable")
45
+ s.apt_install("curl", "git", "wget", fast=True)
46
+
47
+ with s.cache("/root/.cache/uv"), s.run() as r:
48
+ r.uv("pip", "install", "-U", "numpy", "pandas")
49
+ ```
50
+
51
+ Render it:
52
+
53
+ ```bash
54
+ docker-dsl my_recipe --gpu=true --out Dockerfile
55
+ ```
56
+
57
+ Or from Python:
58
+
59
+ ```python
60
+ from docker_dsl import Dockerfile
61
+ import my_recipe
62
+
63
+ with Dockerfile(my_recipe) as d:
64
+ gpu_text = d.render(gpu=True)
65
+ cpu_text = d.render(gpu=False)
66
+ ```
67
+
68
+ ## What problems does this solve?
69
+
70
+ - **`RUN` chains glued with `&&` are fragile.** The run builder accumulates
71
+ commands in Python — `r.git("clone", url)`, `r.make("-j$(nproc)")` — and
72
+ emits one correct `RUN` instruction, with `cd` scoping restored
73
+ automatically.
74
+ - **Variants drift apart.** A GPU and a CPU image usually means two copied
75
+ Dockerfiles diverging over time. Here one recipe takes `--gpu=true|false`
76
+ and renders both from the same code path.
77
+ - **No abstraction in raw Dockerfiles.** Recipes are plain Python modules:
78
+ factor repeated setup into context managers and helper functions, loop and
79
+ branch where the build genuinely varies.
80
+ - **BuildKit mounts are easy to get wrong.** `s.cache(...)`, `s.secret(...)`,
81
+ and `s.bind(...)` are scoped context managers — every `RUN` inside the
82
+ scope picks up the active mounts, and nothing leaks outside it.
83
+
84
+ ## Core concepts
85
+
86
+ ### Two-pass execution
87
+
88
+ When a recipe is first imported, docker-dsl is in **discovery pass**: the
89
+ module body runs but every DSL call is a no-op. `ctx.register(...)` is used
90
+ in this pass to populate a schema of config fields.
91
+
92
+ When `Dockerfile(module).render(**config)` is called, the DSL enters the
93
+ **render pass**: it validates `config` against the registered schema, sets
94
+ up ContextVars, and re-executes the module. Every `with Stage(...) as s:`
95
+ and `s.<method>(...)` now accumulates into the active graph. `ctx.gpu`
96
+ returns the validated config value.
97
+
98
+ This design lets you write the recipe as plain top-level Python — no magic,
99
+ no decorators, no entrypoint functions.
100
+
101
+ ### Config fields
102
+
103
+ ```python
104
+ ctx.register("gpu", bool)
105
+ ctx.register("tag", str)
106
+ ```
107
+
108
+ Every registered field is required at render time. Pydantic validates the
109
+ values before the render pass runs, so type errors surface with clear
110
+ messages.
111
+
112
+ ### Stages
113
+
114
+ ```python
115
+ with Stage("ubuntu:24.04") as base: # FROM ubuntu:24.04 AS base
116
+ base.workdir("/root")
117
+
118
+ with base.stage() as builder: # FROM base AS builder
119
+ builder.run("make", "all")
120
+
121
+ with base.stage() as release: # FROM base AS release
122
+ release.copy("/out/bin", stage=builder)
123
+ ```
124
+
125
+ Child stage names are inferred from the `as <name>` target via the `executing`
126
+ library. Pass `name="..."` to override.
127
+
128
+ ### Run builder
129
+
130
+ `s.run()` as a context manager accumulates shell commands into a single
131
+ `RUN` instruction:
132
+
133
+ ```python
134
+ with s.run() as r:
135
+ r.git("clone", "https://example.com/repo.git")
136
+ with r.cd("repo"):
137
+ r.make("-j$(nproc)")
138
+ r.make("install")
139
+ r("(cd subdir && python setup.py install)") # raw fallback
140
+ ```
141
+
142
+ Command methods dispatch via `__getattr__` — any attribute name becomes the
143
+ shell binary. `r.cd(path)` works both as a statement (subsequent commands
144
+ stay in that directory) and as a context manager (scope-restores on exit via
145
+ `cd -`).
146
+
147
+ Echo redirects compose naturally:
148
+
149
+ ```python
150
+ with s.run() as r:
151
+ r.echo("pillow>=11.1.0") >> "/root/overrides.txt" # append
152
+ r.echo("numpy<3.0.0,>=1.26.4") > "/etc/pip/constraint.txt" # truncate
153
+ ```
154
+
155
+ ### Mounts
156
+
157
+ `s.cache(target, *, lock=False)`, `s.secret(id, *, target=...)`,
158
+ `s.bind(source=..., target=...)` return context managers that push a mount
159
+ onto the stage's stack. Any `RUN` emitted inside the scope picks up all
160
+ active mounts.
161
+
162
+ ```python
163
+ with s.cache("/root/.cache/uv"), s.secret("aws", target="/root/.aws/credentials"):
164
+ with s.run() as r:
165
+ r.uv("pip", "install", "-U", "torch")
166
+ ```
167
+
168
+ ### Smart apt
169
+
170
+ `r.apt_install(...)`, `r.add_apt_ppa(...)`, and `r.add_apt_repo(...)` are
171
+ methods on `RunBuilder` that write directly to the command list.
172
+ `apt-get update -y` is inserted automatically before the first install and
173
+ again after any dirty-marking operation (new PPA, new repo). Arbitrary
174
+ commands (cleanup, setup scripts) are just `r(...)` calls.
175
+
176
+ ```python
177
+ with s.cache("/var/cache/apt", lock=True), s.cache("/var/lib/apt", lock=True), s.run() as r:
178
+ r.apt_install("software-properties-common")
179
+ r.add_apt_ppa("ppa:apt-fast/stable")
180
+ r.apt_install("apt-fast", "curl", "wget", fast=True)
181
+ r("rm -rf /tmp/*")
182
+ ```
183
+
184
+ ### Reusable helpers
185
+
186
+ Recipes can compose their own context managers:
187
+
188
+ ```python
189
+ from contextlib import contextmanager
190
+
191
+ @contextmanager
192
+ def sccache(stage):
193
+ with stage.secret("aws", target="/root/.aws/credentials"):
194
+ yield
195
+
196
+ with Stage("ubuntu:24.04") as s:
197
+ with sccache(s), s.run() as r:
198
+ r.uv("pip", "install", "-U", "torch")
199
+ ```
200
+
201
+ ## CLI
202
+
203
+ ```
204
+ docker-dsl <module.path> [--<field>=<value> ...] [--out PATH]
205
+ ```
206
+
207
+ (`python -m docker_dsl` is equivalent.) Arguments are built dynamically from
208
+ the fields registered by the recipe. A `--out` argument is always available;
209
+ if omitted, the rendered Dockerfile is written to stdout. Bool fields accept
210
+ `true`/`false`/`1`/`0`/`yes`/`no`.
211
+
212
+ Validation errors surface with structured Pydantic messages naming the
213
+ missing or wrong-typed fields.
214
+
215
+ ## Examples
216
+
217
+ See [`examples/`](examples) for self-contained recipes:
218
+
219
+ - `minimal.py` — the shortest possible recipe
220
+ - `multi_stage.py` — builder + release pattern with `COPY --from`
221
+ - `apt_smart.py` — demonstrating the apt buffer flush rules
222
+
223
+ Run them via the CLI:
224
+
225
+ ```bash
226
+ docker-dsl examples.minimal --tag=dev --out Dockerfile.min
227
+ docker-dsl examples.multi_stage --release=true --out Dockerfile.ms
228
+ ```
229
+
230
+ ## Editor completions
231
+
232
+ `RunBuilder` uses `__getattr__` for dynamic shell-command dispatch, which
233
+ means editors have no static information about available commands. To fix
234
+ this, docker-dsl ships a generated `builder.pyi` type stub that declares
235
+ every system command as a method on `RunBuilder`.
236
+
237
+ Commands with bash programmable completions get `@overload` stubs with
238
+ `Literal` subcommands and typed flag kwargs. Commands without completions
239
+ get their flags extracted from man pages. All others get a plain
240
+ `*args: str, **kwargs: str | bool` catch-all.
241
+
242
+ Regenerate after installing new tools:
243
+
244
+ ```bash
245
+ python -m docker_dsl.stubgen
246
+ ```
247
+
248
+ The generator:
249
+ 1. Runs `bash -lic 'compgen -c'` to enumerate commands
250
+ 2. Invokes bash completion functions to extract subcommands + flags
251
+ 3. Falls back to `man <cmd> | col -b` (parallelized across CPU cores) for commands without bash completions
252
+ 4. Parses `builder.py` via `ast` to preserve real method signatures
253
+ 5. Writes `builder.pyi` with `@generated` header
254
+
255
+ Flags become keyword arguments with underscore-to-hyphen conversion:
256
+ `r.git("clone", url, depth="1", verbose=True)` → `git clone <url> --depth 1 --verbose`.
257
+
258
+ ## Docs
259
+
260
+ [Read the docs](https://yasyf.github.io/docker-dsl/) for the full guide and API reference.
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from docker_dsl.context import context, rendering
4
+ from docker_dsl.core import Dockerfile
5
+ from docker_dsl.stage import Stage
6
+ from docker_dsl.state import current_stage
7
+
8
+ # Load-bearing for the docs site: without it, great-docs walks every public
9
+ # symbol, including the ~4500 generated methods in builder.pyi.
10
+ __all__ = ["Dockerfile", "Stage", "context", "current_stage", "rendering"]
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib
5
+ import sys
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from docker_dsl.context import Registry
11
+ from docker_dsl.core import Dockerfile
12
+
13
+
14
+ class Main:
15
+ @classmethod
16
+ def parse_bool(cls, value: str) -> bool:
17
+ match value.lower():
18
+ case "true" | "1" | "yes":
19
+ return True
20
+ case "false" | "0" | "no":
21
+ return False
22
+ raise argparse.ArgumentTypeError(f"expected true/false, got {value!r}")
23
+
24
+ @classmethod
25
+ def type_for(cls, type_: type) -> Callable[[str], Any]:
26
+ return cls.parse_bool if type_ is bool else type_
27
+
28
+ @classmethod
29
+ def run(cls, argv: list[str] | None = None) -> None:
30
+ args = sys.argv[1:] if argv is None else argv
31
+ usage = "usage: docker-dsl <module.path> [--<key>=<value> ...] [--out PATH]"
32
+ if args and args[0] in ("-h", "--help"):
33
+ print(usage)
34
+ return
35
+ if not args:
36
+ raise SystemExit(usage)
37
+ module_path, *rest = args
38
+ module = importlib.import_module(module_path)
39
+ schema = Registry.get(module)
40
+
41
+ parser = argparse.ArgumentParser(prog=f"python -m docker_dsl {module_path}")
42
+ parser.add_argument("--out", type=Path, default=None)
43
+ for name, type_ in schema.items():
44
+ parser.add_argument(f"--{name}", type=cls.type_for(type_), required=True)
45
+
46
+ namespace = parser.parse_args(rest)
47
+ out: Path | None = namespace.out
48
+ config_values = {name: getattr(namespace, name) for name in schema}
49
+
50
+ text = Dockerfile(module).render(path=out, **config_values)
51
+ if out is None:
52
+ sys.stdout.write(text)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ Main.run()