compose2pod 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.
@@ -0,0 +1,13 @@
1
+ """compose2pod: convert a Docker Compose file into a single-Podman-pod run script."""
2
+
3
+ from compose2pod.emit import EmitOptions, emit_script
4
+ from compose2pod.exceptions import UnsupportedComposeError
5
+ from compose2pod.parsing import validate
6
+
7
+
8
+ __all__ = [
9
+ "EmitOptions",
10
+ "UnsupportedComposeError",
11
+ "emit_script",
12
+ "validate",
13
+ ]
@@ -0,0 +1,9 @@
1
+ """python -m compose2pod entry point."""
2
+
3
+ import sys
4
+
5
+ from compose2pod.cli import main
6
+
7
+
8
+ if __name__ == "__main__": # pragma: no cover - exercised via subprocess in tests
9
+ sys.exit(main())
compose2pod/cli.py ADDED
@@ -0,0 +1,110 @@
1
+ """Command-line interface: read a compose document and emit the pod script."""
2
+
3
+ import argparse
4
+ import contextlib
5
+ import json
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from types import ModuleType
10
+ from typing import Any
11
+
12
+ from compose2pod.emit import EmitOptions, emit_script
13
+ from compose2pod.exceptions import UnsupportedComposeError
14
+ from compose2pod.parsing import validate
15
+
16
+
17
+ _yaml: ModuleType | None = None
18
+ with contextlib.suppress(ImportError): # the optional [yaml] extra is not installed
19
+ import yaml as _yaml
20
+
21
+
22
+ POD_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
23
+
24
+
25
+ def _load_yaml(text: str) -> Any: # noqa: ANN401 - returns arbitrary parsed compose data
26
+ if _yaml is None:
27
+ msg = "YAML input requires the 'yaml' extra: pip install compose2pod[yaml] (or pipe JSON via yq)"
28
+ raise UnsupportedComposeError(msg)
29
+ try:
30
+ return _yaml.safe_load(text)
31
+ except _yaml.YAMLError as error:
32
+ msg = f"invalid YAML: {error}"
33
+ raise UnsupportedComposeError(msg) from error
34
+
35
+
36
+ def _read_compose(text: str, fmt: str) -> Any: # noqa: ANN401 - returns arbitrary parsed compose data
37
+ if fmt == "json":
38
+ return json.loads(text)
39
+ if fmt == "yaml":
40
+ return _load_yaml(text)
41
+ try:
42
+ return json.loads(text)
43
+ except json.JSONDecodeError:
44
+ return _load_yaml(text)
45
+
46
+
47
+ def main(argv: list[str] | None = None) -> int:
48
+ parser = argparse.ArgumentParser(
49
+ prog="compose2pod",
50
+ description="Convert a Docker Compose document to a podman-pod run script (stdout).",
51
+ )
52
+ parser.add_argument("file", nargs="?", help="compose file to read (default: stdin)")
53
+ parser.add_argument("--target", required=True, help="service to run in the foreground with --command")
54
+ parser.add_argument("--image", required=True, help="CI image replacing services that have a build section")
55
+ parser.add_argument("--project-dir", default=".", help="host path relative volume/env_file sources resolve to")
56
+ parser.add_argument("--command", default="", help="shell command overriding the target service command")
57
+ parser.add_argument("--pod-name", default="test-pod")
58
+ parser.add_argument("--format", choices=("auto", "json", "yaml"), default="auto")
59
+ parser.add_argument(
60
+ "--artifact",
61
+ action="append",
62
+ default=[],
63
+ metavar="SRC:DST",
64
+ help="file to podman-cp out of the target container after it exits",
65
+ )
66
+ parser.add_argument(
67
+ "--allow-exit-code",
68
+ type=int,
69
+ action="append",
70
+ default=[],
71
+ help="target exit code treated as success in addition to 0",
72
+ )
73
+ args = parser.parse_args(argv)
74
+ if not POD_NAME_PATTERN.match(args.pod_name):
75
+ sys.stderr.write(f"compose2pod: error: invalid pod name {args.pod_name!r}\n")
76
+ return 2
77
+ if args.file:
78
+ try:
79
+ text = Path(args.file).read_text()
80
+ except OSError as error:
81
+ sys.stderr.write(f"compose2pod: error: could not read file: {error}\n")
82
+ return 2
83
+ else:
84
+ text = sys.stdin.read()
85
+ try:
86
+ compose = _read_compose(text, args.format)
87
+ except (json.JSONDecodeError, UnsupportedComposeError) as error:
88
+ sys.stderr.write(f"compose2pod: error: could not parse compose input: {error}\n")
89
+ return 2
90
+ try:
91
+ warnings = validate(compose)
92
+ script = emit_script(
93
+ compose=compose,
94
+ options=EmitOptions(
95
+ target=args.target,
96
+ ci_image=args.image,
97
+ command=args.command,
98
+ pod=args.pod_name,
99
+ project_dir=args.project_dir,
100
+ artifacts=args.artifact,
101
+ allow_exit_codes=args.allow_exit_code,
102
+ ),
103
+ )
104
+ except UnsupportedComposeError as error:
105
+ sys.stderr.write(f"compose2pod: error: {error}\n")
106
+ return 2
107
+ for warning in warnings:
108
+ sys.stderr.write(f"compose2pod: {warning}\n")
109
+ sys.stdout.write(script)
110
+ return 0
compose2pod/emit.py ADDED
@@ -0,0 +1,171 @@
1
+ """Render the podman-pod test script for a target service and its dependencies."""
2
+
3
+ import dataclasses
4
+ import shlex
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from compose2pod.graph import depends_on, hostnames, startup_order
9
+ from compose2pod.healthcheck import health_cmd, interval_seconds
10
+
11
+
12
+ HEALTHY_WAIT_BUDGET_SECONDS = 120
13
+
14
+
15
+ def image_for(svc: dict[str, Any], ci_image: str) -> str:
16
+ """Services with a build section run the freshly built CI image."""
17
+ if "build" in svc:
18
+ return ci_image
19
+ return svc["image"]
20
+
21
+
22
+ def command_tokens(svc: dict[str, Any]) -> list[str]:
23
+ """Service command as argv tokens; compose string form means shell form."""
24
+ command = svc.get("command")
25
+ if command is None:
26
+ return []
27
+ if isinstance(command, str):
28
+ return ["/bin/sh", "-c", command]
29
+ return list(command)
30
+
31
+
32
+ def _add_health_flags(flags: list[str], healthcheck: dict[str, Any]) -> None:
33
+ """Add healthcheck flags to the flags list."""
34
+ cmd = health_cmd(healthcheck.get("test"))
35
+ if cmd is not None:
36
+ flags += ["--health-cmd", cmd]
37
+ if "timeout" in healthcheck:
38
+ flags += ["--health-timeout", str(healthcheck["timeout"])]
39
+ if "start_period" in healthcheck:
40
+ flags += ["--health-start-period", str(healthcheck["start_period"])]
41
+ if "retries" in healthcheck:
42
+ flags += ["--health-retries", str(healthcheck["retries"])]
43
+
44
+
45
+ def run_flags(name: str, svc: dict[str, Any], pod: str, hosts: list[str], project_dir: str) -> list[str]:
46
+ """Flag tokens (unquoted) for `podman run` of one service."""
47
+ flags = ["--pod", pod, "--name", f"{pod}-{name}"]
48
+ for host in hosts:
49
+ flags += ["--add-host", f"{host}:127.0.0.1"]
50
+ environment = svc.get("environment") or {}
51
+ pairs = environment if isinstance(environment, list) else [f"{k}={v}" for k, v in environment.items()]
52
+ for pair in pairs:
53
+ flags += ["-e", pair]
54
+ env_files = svc.get("env_file") or []
55
+ if isinstance(env_files, str):
56
+ env_files = [env_files]
57
+ for env_file in env_files:
58
+ flags += ["--env-file", str(Path(project_dir, env_file))]
59
+ for volume in svc.get("volumes") or []:
60
+ source, destination = volume.split(":", 1)
61
+ if not source.startswith("/"):
62
+ source = str(Path(project_dir, source))
63
+ flags += ["-v", f"{source}:{destination}"]
64
+ healthcheck = svc.get("healthcheck") or {}
65
+ _add_health_flags(flags, healthcheck)
66
+ return flags
67
+
68
+
69
+ _SCRIPT_HEADER = """\
70
+ #!/bin/sh
71
+ # Generated by compose2pod -- do not edit, regenerate instead.
72
+ set -eu
73
+
74
+ wait_healthy() {
75
+ ctr=$1
76
+ attempts=$2
77
+ interval=$3
78
+ i=0
79
+ while [ "$i" -lt "$attempts" ]; do
80
+ if podman healthcheck run "$ctr"; then
81
+ return 0
82
+ fi
83
+ i=$((i + 1))
84
+ sleep "$interval"
85
+ done
86
+ echo "wait_healthy: $ctr did not become healthy after $attempts attempts" >&2
87
+ podman logs "$ctr" >&2 || true
88
+ return 1
89
+ }
90
+ """
91
+
92
+
93
+ @dataclasses.dataclass(frozen=True)
94
+ class EmitOptions:
95
+ """Options for emit_script rendering."""
96
+
97
+ target: str
98
+ ci_image: str
99
+ command: str
100
+ pod: str
101
+ project_dir: str
102
+ artifacts: list[str]
103
+ allow_exit_codes: list[int]
104
+
105
+
106
+ def _render(tokens: list[str]) -> str:
107
+ return " ".join(shlex.quote(token) for token in tokens)
108
+
109
+
110
+ def _run_tokens(name: str, services: dict[str, Any], options: EmitOptions, hosts: list[str]) -> list[str]:
111
+ svc = services[name]
112
+ tokens = run_flags(name, svc, options.pod, hosts, options.project_dir)
113
+ tokens.append(image_for(svc, options.ci_image))
114
+ if name == options.target and options.command:
115
+ tokens.extend(shlex.split(options.command))
116
+ else:
117
+ tokens.extend(command_tokens(svc))
118
+ return tokens
119
+
120
+
121
+ def _emit_target(lines: list[str], run_tokens: list[str], options: EmitOptions) -> None:
122
+ target_ctr = shlex.quote(f"{options.pod}-{options.target}")
123
+ lines.append("rc=0")
124
+ lines.append(f"podman run {_render(run_tokens)} || rc=$?")
125
+ for artifact in options.artifacts:
126
+ source, destination = artifact.split(":", 1)
127
+ lines.append(f"podman cp {target_ctr}:{shlex.quote(source)} {shlex.quote(destination)} || true")
128
+ allowed = "|".join(str(code) for code in [0, *options.allow_exit_codes])
129
+ lines.append('case "$rc" in')
130
+ lines.append(f" {allowed}) ;;")
131
+ lines.append(' *) echo "target service failed with exit code $rc" >&2')
132
+ lines.append(
133
+ " podman inspect --format "
134
+ "'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' " + target_ctr + " >&2 || true"
135
+ )
136
+ lines.append(" podman ps -a --format '{{.Names}} {{.Status}}' >&2 || true")
137
+ lines.append(' exit "$rc" ;;')
138
+ lines.append("esac")
139
+
140
+
141
+ def emit_script(compose: dict[str, Any], options: EmitOptions) -> str:
142
+ """Render the full pod test script for `target` and its dependency closure."""
143
+ services = compose["services"]
144
+ hosts = hostnames(services)
145
+ order = startup_order(services, options.target)
146
+ completion_gated = {
147
+ dep
148
+ for svc in services.values()
149
+ for dep, condition in depends_on(svc).items()
150
+ if condition == "service_completed_successfully"
151
+ }
152
+
153
+ lines = [_SCRIPT_HEADER]
154
+ lines.append(f"trap 'podman pod rm -f {shlex.quote(options.pod)} >/dev/null 2>&1 || true' EXIT")
155
+ lines.append(f"podman pod create --name {shlex.quote(options.pod)}")
156
+ waited: set[str] = set()
157
+ for name in order:
158
+ for dep, condition in depends_on(services[name]).items():
159
+ if condition == "service_healthy" and dep not in waited:
160
+ interval = interval_seconds((services[dep].get("healthcheck") or {}).get("interval"))
161
+ attempts = max(HEALTHY_WAIT_BUDGET_SECONDS // interval, 1)
162
+ lines.append(f"wait_healthy {shlex.quote(f'{options.pod}-{dep}')} {attempts} {interval}")
163
+ waited.add(dep)
164
+ run_tokens = _run_tokens(name, services, options, hosts)
165
+ if name == options.target:
166
+ _emit_target(lines, run_tokens, options)
167
+ elif name in completion_gated:
168
+ lines.append(f"podman run --rm {_render(run_tokens)}")
169
+ else:
170
+ lines.append(f"podman run -d {_render(run_tokens)}")
171
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,5 @@
1
+ """Exceptions for compose2pod."""
2
+
3
+
4
+ class UnsupportedComposeError(Exception):
5
+ """Raised when the compose file uses a construct outside the supported subset."""
compose2pod/graph.py ADDED
@@ -0,0 +1,52 @@
1
+ """Dependency graph: normalize depends_on, collect hostnames, compute startup order."""
2
+
3
+ from typing import Any, cast
4
+
5
+ from compose2pod.exceptions import UnsupportedComposeError
6
+
7
+
8
+ def depends_on(svc: dict[str, Any]) -> dict[str, str]:
9
+ """Normalize dependencies of a service to a name -> condition mapping."""
10
+ deps = svc.get("depends_on") or {}
11
+ if isinstance(deps, list):
12
+ return cast(dict[str, str], dict.fromkeys(deps, "service_started"))
13
+ return {name: spec.get("condition", "service_started") for name, spec in deps.items()}
14
+
15
+
16
+ def hostnames(services: dict[str, Any]) -> list[str]:
17
+ """All names other services may use to reach a service: names, then aliases."""
18
+ names = list(services)
19
+ for svc in services.values():
20
+ networks = svc.get("networks")
21
+ if isinstance(networks, dict):
22
+ for network in networks.values():
23
+ if isinstance(network, dict):
24
+ names.extend(network.get("aliases") or [])
25
+ return names
26
+
27
+
28
+ def startup_order(services: dict[str, Any], target: str) -> list[str]:
29
+ """Dependency closure of target in start order (dependencies first, target last)."""
30
+ if target not in services:
31
+ msg = f"target service '{target}' not found"
32
+ raise UnsupportedComposeError(msg)
33
+ order: list[str] = []
34
+ state: dict[str, str] = {}
35
+
36
+ def visit(name: str) -> None:
37
+ if state.get(name) == "visiting":
38
+ msg = f"dependency cycle involving '{name}'"
39
+ raise UnsupportedComposeError(msg)
40
+ if state.get(name) == "done":
41
+ return
42
+ if name not in services:
43
+ msg = f"unknown dependency '{name}'"
44
+ raise UnsupportedComposeError(msg)
45
+ state[name] = "visiting"
46
+ for dep in depends_on(services[name]):
47
+ visit(dep)
48
+ state[name] = "done"
49
+ order.append(name)
50
+
51
+ visit(target)
52
+ return order
@@ -0,0 +1,54 @@
1
+ """Healthcheck translation: compose healthcheck -> podman --health-* values."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from compose2pod.exceptions import UnsupportedComposeError
7
+
8
+
9
+ _CMD_MIN_LENGTH = 2
10
+
11
+
12
+ def has_healthcheck(svc: dict[str, Any]) -> bool:
13
+ """Report whether the service defines a healthcheck with a non-disabled test."""
14
+ test = (svc.get("healthcheck") or {}).get("test")
15
+ return test is not None and test not in ("NONE", ["NONE"])
16
+
17
+
18
+ def health_cmd(test: object) -> str | None:
19
+ """Compose healthcheck `test` value to a podman --health-cmd value."""
20
+ if test is None or test in ("NONE", ["NONE"]):
21
+ return None
22
+ if isinstance(test, str):
23
+ return test
24
+ if not isinstance(test, list) or not test:
25
+ msg = f"unsupported healthcheck test: {test!r}"
26
+ raise UnsupportedComposeError(msg)
27
+ kind = test[0]
28
+ if kind == "CMD-SHELL":
29
+ if len(test) < _CMD_MIN_LENGTH:
30
+ msg = f"unsupported healthcheck test: {test!r}"
31
+ raise UnsupportedComposeError(msg)
32
+ return test[1] # ty: ignore
33
+ if kind == "CMD":
34
+ if len(test) < _CMD_MIN_LENGTH:
35
+ msg = f"unsupported healthcheck test: {test!r}"
36
+ raise UnsupportedComposeError(msg)
37
+ return json.dumps(test[1:])
38
+ msg = f"unsupported healthcheck test kind: {kind!r}"
39
+ raise UnsupportedComposeError(msg)
40
+
41
+
42
+ def interval_seconds(duration: object) -> int:
43
+ """Compose duration ('1s', '2m', '500ms', int) to whole seconds, minimum 1."""
44
+ if duration is None:
45
+ return 1
46
+ if isinstance(duration, (int, float)):
47
+ return max(int(duration), 1)
48
+ text = str(duration).strip()
49
+ if text.endswith("ms"):
50
+ return max(int(float(text[:-2]) / 1000), 1)
51
+ if text.endswith("m"):
52
+ return max(int(float(text[:-1])) * 60, 1)
53
+ text = text.removesuffix("s")
54
+ return max(int(float(text)), 1)
compose2pod/parsing.py ADDED
@@ -0,0 +1,86 @@
1
+ """Validate a compose document against the supported subset."""
2
+
3
+ from typing import Any
4
+
5
+ from compose2pod.exceptions import UnsupportedComposeError
6
+ from compose2pod.graph import depends_on
7
+ from compose2pod.healthcheck import has_healthcheck
8
+
9
+
10
+ SUPPORTED_SERVICE_KEYS = {
11
+ "image",
12
+ "build",
13
+ "command",
14
+ "environment",
15
+ "env_file",
16
+ "volumes",
17
+ "healthcheck",
18
+ "depends_on",
19
+ "networks",
20
+ }
21
+ IGNORED_SERVICE_KEYS = {"ports", "restart", "stdin_open", "tty"}
22
+ SUPPORTED_HEALTHCHECK_KEYS = {"test", "interval", "timeout", "retries", "start_period"}
23
+ SUPPORTED_TOP_LEVEL_KEYS = {"services", "version", "name", "networks"}
24
+ DEPENDS_ON_CONDITIONS = {"service_started", "service_healthy", "service_completed_successfully"}
25
+
26
+
27
+ def _validate_service(name: str, svc: dict[str, Any]) -> list[str]:
28
+ """Validate one service; returns warnings, raises UnsupportedComposeError."""
29
+ warnings: list[str] = []
30
+ for key in sorted(svc):
31
+ if key in IGNORED_SERVICE_KEYS:
32
+ warnings.append(f"service {name!r}: ignoring '{key}'")
33
+ elif key not in SUPPORTED_SERVICE_KEYS:
34
+ msg = f"service {name!r}: unsupported key '{key}'"
35
+ raise UnsupportedComposeError(msg)
36
+ for key in sorted(svc.get("healthcheck") or {}):
37
+ if key not in SUPPORTED_HEALTHCHECK_KEYS:
38
+ msg = f"service {name!r}: unsupported healthcheck key '{key}'"
39
+ raise UnsupportedComposeError(msg)
40
+ for volume in svc.get("volumes") or []:
41
+ if not isinstance(volume, str):
42
+ msg = f"service {name!r}: only short volume syntax is supported"
43
+ raise UnsupportedComposeError(msg)
44
+ source = volume.split(":", 1)[0]
45
+ if not source.startswith((".", "/")):
46
+ msg = f"service {name!r}: named volume '{source}' is not supported (bind mounts only)"
47
+ raise UnsupportedComposeError(msg)
48
+ return warnings
49
+
50
+
51
+ def _validate_depends_on(services: dict[str, Any]) -> None:
52
+ """Cross-service depends_on checks: known conditions, service_healthy needs a healthcheck."""
53
+ for name, svc in services.items():
54
+ for dep, condition in depends_on(svc).items():
55
+ if condition not in DEPENDS_ON_CONDITIONS:
56
+ msg = f"service {name!r}: depends_on {dep!r} has unsupported condition {condition!r}"
57
+ raise UnsupportedComposeError(msg)
58
+ if condition == "service_healthy" and dep in services and not has_healthcheck(services[dep]):
59
+ msg = f"service {name!r}: depends on {dep!r} (service_healthy) but {dep!r} has no healthcheck"
60
+ raise UnsupportedComposeError(msg)
61
+
62
+
63
+ def validate(compose: dict[str, Any]) -> list[str]:
64
+ """Check the compose document against the supported subset.
65
+
66
+ Returns human-readable warnings for ignored constructs.
67
+ Raises UnsupportedComposeError for anything that would change behavior silently.
68
+ """
69
+ if not isinstance(compose, dict):
70
+ msg = f"compose document must be a mapping, got {type(compose).__name__}"
71
+ raise UnsupportedComposeError(msg)
72
+ warnings: list[str] = []
73
+ unknown_top = set(compose) - SUPPORTED_TOP_LEVEL_KEYS
74
+ if unknown_top:
75
+ msg = f"unsupported top-level keys: {sorted(unknown_top)}"
76
+ raise UnsupportedComposeError(msg)
77
+ if "networks" in compose:
78
+ warnings.append("ignoring top-level 'networks' (all services share the pod namespace)")
79
+ services = compose.get("services") or {}
80
+ if not services:
81
+ msg = "no services defined"
82
+ raise UnsupportedComposeError(msg)
83
+ for name, svc in services.items():
84
+ warnings.extend(_validate_service(name, svc))
85
+ _validate_depends_on(services)
86
+ return warnings
compose2pod/py.typed ADDED
File without changes
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: compose2pod
3
+ Version: 0.1.0
4
+ Summary: Convert a Docker Compose file into a script that runs its services as a single Podman pod
5
+ Keywords: podman,docker-compose,compose,pod,ci,testing,containers
6
+ Author: Artur Shiriev
7
+ Author-email: Artur Shiriev <me@shiriev.ru>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Dist: pyyaml>=6 ; extra == 'yaml'
20
+ Requires-Python: >=3.10, <4
21
+ Project-URL: Repository, https://github.com/modern-python/compose2pod
22
+ Project-URL: Issues, https://github.com/modern-python/compose2pod/issues
23
+ Project-URL: Changelog, https://github.com/modern-python/compose2pod/releases
24
+ Provides-Extra: yaml
25
+ Description-Content-Type: text/markdown
26
+
27
+ # compose2pod
28
+
29
+ Convert a Docker Compose file into a POSIX `sh` script that runs its services as a **single Podman pod**.
30
+
31
+ Built for CI and test environments where you can't use `docker compose` or `podman kube play`:
32
+
33
+ - **No bridge networking / netavark.** Unprivileged CI containers often have a read-only `/proc/sys`, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over `127.0.0.1`, and names resolve via `--add-host`.
34
+ - **No systemd.** Podman healthchecks are normally scheduled by systemd timers. compose2pod gates startup by polling `podman healthcheck run` directly, so `depends_on: service_healthy` works without systemd.
35
+ - **No heavy runtime.** The core is stdlib-only — no dependencies, no compiled wheels — so it installs and runs in minimal Python images.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install compose2pod # core: reads compose as JSON
41
+ pip install compose2pod[yaml] # optional: read YAML directly (adds PyYAML)
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ # YAML directly (needs the [yaml] extra)
48
+ compose2pod docker-compose.yml --target app --image myimage:ci > run.sh
49
+
50
+ # Or stay dependency-free by piping JSON (e.g. via yq)
51
+ yq -o=json '.' docker-compose.yml | compose2pod --target app --image myimage:ci > run.sh
52
+
53
+ sh ./run.sh
54
+ ```
55
+
56
+ ## Supported compose subset
57
+
58
+ compose2pod supports an honest subset and errors clearly on anything outside it. See the design spec for the full matrix: `image`/`build`, `command`, `environment`/`env_file`, short-form bind `volumes`, `healthcheck` (CMD/CMD-SHELL), `depends_on` (all conditions), and network `aliases`.
59
+
60
+ ## Status
61
+
62
+ Beta. Part of the [modern-python](https://github.com/modern-python) family. MIT licensed.
@@ -0,0 +1,13 @@
1
+ compose2pod/__init__.py,sha256=j8ci48inaBz3aDM9LH4YeAFLA6G3CyvBn7pNgWOiEv8,342
2
+ compose2pod/__main__.py,sha256=b6KTiotqkNZUYrbmX4choezIzSj4MMrC3yojbQ6icJM,193
3
+ compose2pod/cli.py,sha256=tqVBCTCbg5mJFj6wtIa5yU97AYOa9TC1y1zFxCBZ7oc,4012
4
+ compose2pod/emit.py,sha256=AeeK1-7zFMs_rfPCRdJib6PshO1VBKyQ1n7ubZ9fmq8,6236
5
+ compose2pod/exceptions.py,sha256=lLoInp8p2y_zkYf0stS2UiweHhqUfTfBHBYcafTeJGQ,164
6
+ compose2pod/graph.py,sha256=8UXWjEYQ7RvWmH5X_t-w33HTca1oL_m7_M-mpomVB_0,1909
7
+ compose2pod/healthcheck.py,sha256=Kvte7Tyv28KxlGA7XuOhFWPO7MdKn5A36ty4kpM7LpY,1886
8
+ compose2pod/parsing.py,sha256=1NuI5QqdsnnE-84Nmp_3KC4ZCGc8aCBate18-XEn4DE,3698
9
+ compose2pod/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ compose2pod-0.1.0.dist-info/WHEEL,sha256=uOqnPWqgFlbov4NeTCercq7cBQ2UN7xh5fiW55lOnAg,81
11
+ compose2pod-0.1.0.dist-info/entry_points.txt,sha256=6Uy7VRCbWrx48fbjR0FkyZLbLW6s0PpkDKuRk4ZcsDw,54
12
+ compose2pod-0.1.0.dist-info/METADATA,sha256=YfdRQpZp0vohOYZpR2Fr1mFxVxhtr7zRgrQRXIzzl9s,2867
13
+ compose2pod-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.26
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ compose2pod = compose2pod.cli:main
3
+