docker-dsl 0.1.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.
docker_dsl/__init__.py ADDED
@@ -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"]
docker_dsl/__main__.py ADDED
@@ -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()
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # Invoked by stubgen.py via: bash -lic 'source _completions.sh <output.json>'
3
+ # Must run via -c so bash stays fully interactive for completion loading.
4
+
5
+ OUTPUT="${1:?usage: source _completions.sh <output.json>}"
6
+
7
+ # Collect all command names
8
+ mapfile -t ALL_CMDS < <(compgen -c | sort -u)
9
+
10
+ # Trigger lazy loading for common dev tools that might not be registered yet
11
+ PRIORITY_CMDS=(git make cmake curl tar docker uv pip npm cargo gcc g++ clang
12
+ python python3 ruby node rustc go java javac scp ssh rsync sed awk
13
+ find xargs sort uniq wc head tail cat less grep diff patch chmod chown
14
+ cp mv rm mkdir rmdir ln touch kill ps mount umount df du free top htop
15
+ systemctl journalctl apt dpkg yum dnf pacman brew port)
16
+
17
+ for cmd in "${PRIORITY_CMDS[@]}"; do
18
+ _completion_loader "$cmd" 2>/dev/null || true
19
+ done
20
+
21
+ # Collect all function-based completion specs
22
+ declare -A COMP_FUNCS
23
+ while IFS= read -r line; do
24
+ fn=$(echo "$line" | sed -n 's/.*-F \([^ ]*\).*/\1/p')
25
+ cmd=$(echo "$line" | awk '{print $NF}')
26
+ [ -n "$fn" ] && [ -n "$cmd" ] && COMP_FUNCS["$cmd"]="$fn"
27
+ done < <(complete -p 2>/dev/null)
28
+
29
+ extract_completions() {
30
+ local cmd="$1" fn="$2"
31
+
32
+ COMPREPLY=()
33
+ COMP_WORDS=("$cmd" "")
34
+ COMP_CWORD=1
35
+ COMP_LINE="$cmd "
36
+ COMP_POINT=$((${#cmd} + 1))
37
+ "$fn" >/dev/null 2>&1 || true
38
+
39
+ local subs=()
40
+ local is_filesystem=true
41
+ for s in "${COMPREPLY[@]}"; do
42
+ s="${s%% }"
43
+ [ -z "$s" ] && continue
44
+ [[ "$s" == */* ]] && continue
45
+ [[ "$s" == *.* ]] && continue
46
+ [ ! -e "$s" ] && is_filesystem=false
47
+ subs+=("$s")
48
+ done
49
+ $is_filesystem && [ ${#subs[@]} -gt 0 ] && subs=()
50
+
51
+ COMPREPLY=()
52
+ COMP_WORDS=("$cmd" "--")
53
+ COMP_CWORD=1
54
+ COMP_LINE="$cmd --"
55
+ COMP_POINT=$((${#cmd} + 3))
56
+ "$fn" >/dev/null 2>&1 || true
57
+
58
+ local flags=()
59
+ for f in "${COMPREPLY[@]}"; do
60
+ f="${f%% }"
61
+ f="${f%%=}"
62
+ [[ "$f" == --* ]] || continue
63
+ flags+=("$f")
64
+ done
65
+
66
+ # Discard if too many subcommands (dictionary/manpage completion noise)
67
+ [ ${#subs[@]} -gt 500 ] && subs=()
68
+ [ ${#subs[@]} -eq 0 ] && [ ${#flags[@]} -eq 0 ] && return
69
+
70
+ local subs_json flags_json
71
+ subs_json=$(printf '%s\n' "${subs[@]}" | sort -u | jq -Rsc 'split("\n") | map(select(. != ""))')
72
+ flags_json=$(printf '%s\n' "${flags[@]}" | sort -u | jq -Rsc 'split("\n") | map(select(. != ""))')
73
+
74
+ printf '"%s": {"subcommands": %s, "flags": %s}' "$cmd" "$subs_json" "$flags_json"
75
+ }
76
+
77
+ {
78
+ first=true
79
+ printf '{"commands": ['
80
+ for cmd in "${ALL_CMDS[@]}"; do
81
+ $first || printf ','
82
+ printf '"%s"' "$cmd"
83
+ first=false
84
+ done
85
+ printf '], "completions": {'
86
+
87
+ first=true
88
+ for cmd in "${!COMP_FUNCS[@]}"; do
89
+ result=$(extract_completions "$cmd" "${COMP_FUNCS[$cmd]}" 2>/dev/null </dev/null) || true
90
+ [ -z "$result" ] && continue
91
+ $first || printf ','
92
+ printf '%s' "$result"
93
+ first=false
94
+ done
95
+
96
+ printf '}}\n'
97
+ } > "$OUTPUT"
docker_dsl/apt.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from docker_dsl.run import ShellCommand
8
+
9
+
10
+ @dataclass(kw_only=True)
11
+ class AptMixin:
12
+ apt_updated: bool = False
13
+ apt_dirty: bool = True
14
+
15
+ def append(self, command: ShellCommand) -> None: ...
16
+
17
+ def apt_install(self, *packages: str, fast: bool = False) -> None:
18
+ from docker_dsl.run import ShellCommand
19
+
20
+ if not self.apt_updated or self.apt_dirty:
21
+ self.append(ShellCommand(raw="apt-get update -y"))
22
+ self.apt_updated = True
23
+ self.apt_dirty = False
24
+ cmd = "apt-fast" if fast else "apt-get"
25
+ self.append(ShellCommand(raw=f"{cmd} install -y --no-install-recommends {' '.join(packages)}"))
26
+
27
+ def add_apt_ppa(self, ppa: str) -> None:
28
+ from docker_dsl.run import ShellCommand
29
+
30
+ self.append(ShellCommand(raw=f"add-apt-repository {ppa} -y"))
31
+ self.apt_dirty = True
32
+
33
+ def add_apt_repo(self, key_url: str, repo_url: str, *, name: str) -> None:
34
+ from docker_dsl.run import ShellCommand
35
+
36
+ self.append(
37
+ ShellCommand(raw=f"wget -O- {key_url} | gpg --dearmor | tee /usr/share/keyrings/{name}.gpg > /dev/null")
38
+ )
39
+ self.append(
40
+ ShellCommand(
41
+ raw=f'echo "deb [signed-by=/usr/share/keyrings/{name}.gpg] {repo_url}" '
42
+ f"| tee /etc/apt/sources.list.d/{name}.list"
43
+ )
44
+ )
45
+ self.apt_dirty = True
docker_dsl/builder.py ADDED
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from types import TracebackType
5
+ from typing import TYPE_CHECKING, Self
6
+
7
+ from docker_dsl.apt import AptMixin
8
+ from docker_dsl.run import CdScope, RedirectableCmd, ShellCommand
9
+
10
+ if TYPE_CHECKING:
11
+ from docker_dsl.run import CmdInvoker
12
+ from docker_dsl.stage import Stage
13
+
14
+
15
+ @dataclass
16
+ class RunBuilder(AptMixin):
17
+ stage: Stage
18
+ commands: list[ShellCommand] = field(default_factory=list)
19
+
20
+ def __enter__(self) -> Self:
21
+ return self
22
+
23
+ def __exit__(
24
+ self,
25
+ exc_type: type[BaseException] | None,
26
+ exc_val: BaseException | None,
27
+ exc_tb: TracebackType | None,
28
+ ) -> None:
29
+ if exc_type is not None or not self.commands:
30
+ return
31
+ self.stage.emit_run(self.build_script())
32
+
33
+ def build_script(self) -> str:
34
+ return " \\\n && ".join(c.render() for c in self.commands)
35
+
36
+ def append(self, command: ShellCommand) -> None:
37
+ self.commands.append(command)
38
+
39
+ # The generated builder.pyi shadows this module for type checkers, so
40
+ # `Self` here is a distinct type from the stub's `RunBuilder` that
41
+ # CmdInvoker & co. are annotated against.
42
+ def __getattr__(self, name: str) -> CmdInvoker:
43
+ if name.startswith("_"):
44
+ raise AttributeError(name)
45
+ from docker_dsl.run import CmdInvoker
46
+
47
+ return CmdInvoker(self, name) # type: ignore
48
+
49
+ def __call__(self, raw: str, *, env: dict[str, str] | None = None) -> None:
50
+ self.append(ShellCommand(raw=raw, env=env or {}))
51
+
52
+ def echo(self, text: str) -> RedirectableCmd:
53
+ return RedirectableCmd(self, text) # type: ignore
54
+
55
+ def cd(self, path: str) -> CdScope:
56
+ return CdScope(self, path) # type: ignore
57
+
58
+ def curl_bash(self, url: str, *, args: tuple[str, ...] = ()) -> None:
59
+ tail = f" -- {' '.join(args)}" if args else ""
60
+ self.append(ShellCommand(raw=f"curl --proto '=https' --tlsv1.2 -sSf {url} | bash -s{tail}"))
61
+
62
+ def install(self, url: str, *, target: str = "/usr/local/bin", strip: int = 1) -> None:
63
+ self.append(ShellCommand(raw=f"curl -fL {url} | tar xz -C {target} --strip-components={strip}"))
64
+
65
+ def fetch_file(self, url: str, dest: str) -> None:
66
+ self.append(ShellCommand(raw=f"mkdir -p $(dirname {dest}) && curl -fsSL {url} -o {dest}"))