docker-composer 2.79.7__tar.gz → 5.0.2__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.
- {docker_composer-2.79.7 → docker_composer-5.0.2}/PKG-INFO +22 -12
- {docker_composer-2.79.7 → docker_composer-5.0.2}/README.md +21 -8
- {docker_composer-2.79.7 → docker_composer-5.0.2}/pyproject.toml +18 -8
- docker_composer-5.0.2/src/docker_composer/__init__.py +6 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/_utils/argument.py +33 -48
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/_utils/generate_class.py +65 -63
- docker_composer-5.0.2/src/docker_composer/_utils/sync_version.py +21 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/base.py +19 -16
- docker_composer-5.0.2/src/docker_composer/runner/cmd/__init__.py +0 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/attach.py +15 -12
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/build.py +32 -18
- docker_composer-5.0.2/src/docker_composer/runner/cmd/commit.py +37 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/config.py +38 -23
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/cp.py +18 -12
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/create.py +23 -17
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/down.py +19 -14
- docker_composer-5.0.2/src/docker_composer/runner/cmd/events.py +33 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/exec.py +18 -14
- docker_composer-5.0.2/src/docker_composer/runner/cmd/export.py +30 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/images.py +14 -11
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/kill.py +14 -11
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/logs.py +17 -14
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/ls.py +16 -13
- docker_composer-5.0.2/src/docker_composer/runner/cmd/pause.py +26 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/port.py +13 -10
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/ps.py +22 -17
- docker_composer-5.0.2/src/docker_composer/runner/cmd/publish.py +40 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/pull.py +17 -14
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/push.py +16 -13
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/restart.py +14 -11
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/rm.py +16 -13
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/run.py +34 -21
- docker_composer-5.0.2/src/docker_composer/runner/cmd/scale.py +29 -0
- docker_composer-5.0.2/src/docker_composer/runner/cmd/start.py +31 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/stats.py +18 -14
- docker_composer-5.0.2/src/docker_composer/runner/cmd/stop.py +28 -0
- docker_composer-5.0.2/src/docker_composer/runner/cmd/top.py +26 -0
- docker_composer-5.0.2/src/docker_composer/runner/cmd/unpause.py +26 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/up.py +55 -40
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/version.py +14 -11
- docker_composer-5.0.2/src/docker_composer/runner/cmd/volumes.py +37 -0
- docker_composer-5.0.2/src/docker_composer/runner/cmd/wait.py +29 -0
- docker_composer-5.0.2/src/docker_composer/runner/cmd/watch.py +35 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/root.py +205 -42
- docker_composer-2.79.7/src/docker_composer/__init__.py +0 -3
- docker_composer-2.79.7/src/docker_composer/runner/cmd/events.py +0 -26
- docker_composer-2.79.7/src/docker_composer/runner/cmd/pause.py +0 -23
- docker_composer-2.79.7/src/docker_composer/runner/cmd/scale.py +0 -26
- docker_composer-2.79.7/src/docker_composer/runner/cmd/start.py +0 -23
- docker_composer-2.79.7/src/docker_composer/runner/cmd/stop.py +0 -25
- docker_composer-2.79.7/src/docker_composer/runner/cmd/top.py +0 -23
- docker_composer-2.79.7/src/docker_composer/runner/cmd/unpause.py +0 -23
- docker_composer-2.79.7/src/docker_composer/runner/cmd/wait.py +0 -26
- docker_composer-2.79.7/src/docker_composer/runner/cmd/watch.py +0 -32
- {docker_composer-2.79.7 → docker_composer-5.0.2}/.gitignore +0 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/LICENSE.txt +0 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/_utils/__init__.py +0 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/py.typed +0 -0
- {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docker-composer
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.2
|
|
4
4
|
Summary: Use docker-compose (V2) from within Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/schollm/docker-composer
|
|
6
6
|
Project-URL: Repository, https://github.com/schollm/docker-composer
|
|
@@ -16,9 +16,6 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
16
|
Classifier: Topic :: Software Development :: Build Tools
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
18
|
Requires-Python: <4.0,>=3.9
|
|
19
|
-
Requires-Dist: attrs>=20.3.0
|
|
20
|
-
Requires-Dist: loguru>=0.5.3
|
|
21
|
-
Requires-Dist: ruff>=0.11.10
|
|
22
19
|
Description-Content-Type: text/markdown
|
|
23
20
|
|
|
24
21
|
# Docker Composer
|
|
@@ -27,6 +24,8 @@ All commands and parameters are exposed as python classes and attributes
|
|
|
27
24
|
to allow for full auto-completion of its parameters with IDEs
|
|
28
25
|
that support it.
|
|
29
26
|
|
|
27
|
+
**Runtime footprint:** This library is **stdlib-only** (zero third-party runtime dependencies).
|
|
28
|
+
|
|
30
29
|
|
|
31
30
|
## Install
|
|
32
31
|
```shell script
|
|
@@ -67,13 +66,24 @@ print(process.stdout.encode("UTF-8"))
|
|
|
67
66
|
|
|
68
67
|
## Develop
|
|
69
68
|
|
|
69
|
+
### Development Setup
|
|
70
|
+
|
|
71
|
+
When developing, the `_utils` module is available for generating the docker_composer
|
|
72
|
+
module from the `docker compose`CLI help output.
|
|
73
|
+
This is not required for normal package usage.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv sync --group dev
|
|
77
|
+
uv run poe generate
|
|
78
|
+
```
|
|
79
|
+
|
|
70
80
|
### Coding Standards
|
|
71
81
|
|
|
72
|
-
| **Type** | Package
|
|
73
|
-
| --------------
|
|
74
|
-
| **Linter** | `
|
|
75
|
-
| **Logging** | `
|
|
76
|
-
| **Packaging** | `uv`
|
|
77
|
-
| **Tests** | `pytest`
|
|
78
|
-
| **Typing** | `mypy`
|
|
79
|
-
| **Imports** | `
|
|
82
|
+
| **Type** | Package | Comment |
|
|
83
|
+
| -------------- |-----------| ------------------------------- |
|
|
84
|
+
| **Linter** | `ruff` | Also for auto-formatted modules |
|
|
85
|
+
| **Logging** | `logging` | |
|
|
86
|
+
| **Packaging** | `uv` | |
|
|
87
|
+
| **Tests** | `pytest` | |
|
|
88
|
+
| **Typing** | `mypy` | Type all methods |
|
|
89
|
+
| **Imports** | `ruff` | |
|
|
@@ -4,6 +4,8 @@ All commands and parameters are exposed as python classes and attributes
|
|
|
4
4
|
to allow for full auto-completion of its parameters with IDEs
|
|
5
5
|
that support it.
|
|
6
6
|
|
|
7
|
+
**Runtime footprint:** This library is **stdlib-only** (zero third-party runtime dependencies).
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
## Install
|
|
9
11
|
```shell script
|
|
@@ -44,13 +46,24 @@ print(process.stdout.encode("UTF-8"))
|
|
|
44
46
|
|
|
45
47
|
## Develop
|
|
46
48
|
|
|
49
|
+
### Development Setup
|
|
50
|
+
|
|
51
|
+
When developing, the `_utils` module is available for generating the docker_composer
|
|
52
|
+
module from the `docker compose`CLI help output.
|
|
53
|
+
This is not required for normal package usage.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv sync --group dev
|
|
57
|
+
uv run poe generate
|
|
58
|
+
```
|
|
59
|
+
|
|
47
60
|
### Coding Standards
|
|
48
61
|
|
|
49
|
-
| **Type** | Package
|
|
50
|
-
| --------------
|
|
51
|
-
| **Linter** | `
|
|
52
|
-
| **Logging** | `
|
|
53
|
-
| **Packaging** | `uv`
|
|
54
|
-
| **Tests** | `pytest`
|
|
55
|
-
| **Typing** | `mypy`
|
|
56
|
-
| **Imports** | `
|
|
62
|
+
| **Type** | Package | Comment |
|
|
63
|
+
| -------------- |-----------| ------------------------------- |
|
|
64
|
+
| **Linter** | `ruff` | Also for auto-formatted modules |
|
|
65
|
+
| **Logging** | `logging` | |
|
|
66
|
+
| **Packaging** | `uv` | |
|
|
67
|
+
| **Tests** | `pytest` | |
|
|
68
|
+
| **Typing** | `mypy` | Type all methods |
|
|
69
|
+
| **Imports** | `ruff` | |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docker-composer"
|
|
3
|
-
version = "
|
|
3
|
+
version = "5.0.2"
|
|
4
4
|
description = "Use docker-compose (V2) from within Python"
|
|
5
5
|
authors = [{ name = "Micha", email = "schollm-git@gmx.com" }]
|
|
6
6
|
requires-python = ">=3.9,<4.0"
|
|
@@ -16,11 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Operating System :: OS Independent",
|
|
17
17
|
"Development Status :: 5 - Production/Stable",
|
|
18
18
|
]
|
|
19
|
-
dependencies = [
|
|
20
|
-
"attrs>=20.3.0",
|
|
21
|
-
"loguru>=0.5.3",
|
|
22
|
-
"ruff>=0.11.10",
|
|
23
|
-
]
|
|
19
|
+
dependencies = []
|
|
24
20
|
|
|
25
21
|
[project.urls]
|
|
26
22
|
Homepage = "https://github.com/schollm/docker-composer"
|
|
@@ -29,10 +25,11 @@ Repository = "https://github.com/schollm/docker-composer"
|
|
|
29
25
|
[dependency-groups]
|
|
30
26
|
dev = [
|
|
31
27
|
"black>=25.1.0",
|
|
32
|
-
"flake8>=6.0.0,<7",
|
|
33
28
|
"isort>=6.0.1",
|
|
29
|
+
"poethepoet>=0.30.0",
|
|
34
30
|
"pytest>=6.1.2",
|
|
35
31
|
"pytest-cov>=6.1.1",
|
|
32
|
+
"ruff>=0.11.10",
|
|
36
33
|
]
|
|
37
34
|
|
|
38
35
|
[tool.hatch.build.targets.sdist]
|
|
@@ -40,6 +37,7 @@ include = ["src/docker_composer"]
|
|
|
40
37
|
|
|
41
38
|
[tool.hatch.build.targets.wheel]
|
|
42
39
|
include = ["src/docker_composer"]
|
|
40
|
+
exclude = ["src/docker_composer/_utils"]
|
|
43
41
|
|
|
44
42
|
[tool.hatch.build.targets.wheel.sources]
|
|
45
43
|
"src/docker_composer" = "docker_composer"
|
|
@@ -54,8 +52,20 @@ addopts = """
|
|
|
54
52
|
--cov=src/docker_composer
|
|
55
53
|
--cov-report=xml:.out/coverage.xml
|
|
56
54
|
--cov-report=html:.out/coverage-html
|
|
57
|
-
--cov-report term-missing
|
|
55
|
+
--cov-report term-missing:skip-covered
|
|
58
56
|
--cov-branch
|
|
59
57
|
--doctest-modules
|
|
60
58
|
"""
|
|
61
59
|
|
|
60
|
+
[tool.poe.tasks]
|
|
61
|
+
ruff-check = { cmd = "uv run ruff check" }
|
|
62
|
+
ruff-format-check = { cmd = "uv run ruff format --check" }
|
|
63
|
+
pytest = { cmd = "uv run pytest" }
|
|
64
|
+
format = { cmd = "uv run ruff format" }
|
|
65
|
+
build = { cmd = "uv build" }
|
|
66
|
+
publish-dist = { cmd = "uv publish" }
|
|
67
|
+
generate-code = { cmd = "uv run python -m docker_composer._utils.generate_class" }
|
|
68
|
+
sync-version = { cmd = "uv run python -m docker_composer._utils.sync_version" }
|
|
69
|
+
publish = { sequence = ["build", "publish-dist"] }
|
|
70
|
+
test = { sequence = ["ruff-check", "ruff-format-check", "pytest $POE_EXTRA_ARGS"] }
|
|
71
|
+
generate = { sequence = ["generate-code", "sync-version"] }
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
"""Helper modules to parse docker compoose --help arguments"""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Iterable, Iterator
|
|
5
5
|
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
6
10
|
OPTION = "OPTION"
|
|
7
11
|
|
|
8
12
|
_TYPE_CONVERSIONS = {
|
|
@@ -16,7 +20,6 @@ _TYPE_CONVERSIONS = {
|
|
|
16
20
|
"LEVEL": int,
|
|
17
21
|
"MEM": int,
|
|
18
22
|
"NAME": str,
|
|
19
|
-
OPTION: bool,
|
|
20
23
|
"PATH": str,
|
|
21
24
|
"SERVICE": str,
|
|
22
25
|
"SIGNAL": int,
|
|
@@ -24,28 +27,34 @@ _TYPE_CONVERSIONS = {
|
|
|
24
27
|
"TIMEOUT": int,
|
|
25
28
|
"TLS_KEY_PATH": str,
|
|
26
29
|
"USER": str,
|
|
27
|
-
"
|
|
28
|
-
"
|
|
30
|
+
"bytes": int,
|
|
31
|
+
"docker": bool,
|
|
32
|
+
"filter": str,
|
|
29
33
|
"int": int,
|
|
30
34
|
"list": list,
|
|
35
|
+
"scale": int,
|
|
31
36
|
"str": str,
|
|
37
|
+
"string": str,
|
|
32
38
|
"stringArray": list[str],
|
|
39
|
+
"type": str,
|
|
33
40
|
"volumes": list[str],
|
|
34
|
-
|
|
41
|
+
OPTION: bool,
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
|
|
38
|
-
@
|
|
45
|
+
@dataclass(frozen=True)
|
|
39
46
|
class Argument:
|
|
40
47
|
arg: str
|
|
41
48
|
type_desc: str
|
|
42
|
-
|
|
49
|
+
type_: type
|
|
43
50
|
description: str
|
|
44
51
|
default: str = ""
|
|
45
52
|
|
|
46
53
|
@property
|
|
47
54
|
def type_str(self) -> str:
|
|
48
|
-
|
|
55
|
+
if self.type_ is object:
|
|
56
|
+
raise ValueError(self.type_)
|
|
57
|
+
return self.type_.__name__
|
|
49
58
|
|
|
50
59
|
@property
|
|
51
60
|
def is_option(self) -> bool:
|
|
@@ -55,28 +64,7 @@ class Argument:
|
|
|
55
64
|
def from_line(line: str) -> "Argument":
|
|
56
65
|
if " " in line:
|
|
57
66
|
return _from_line_has_sep(line)
|
|
58
|
-
|
|
59
|
-
words = iter(line.split())
|
|
60
|
-
has_more = True
|
|
61
|
-
while has_more:
|
|
62
|
-
arg, default_str, has_more = _parse_arg(next(words))
|
|
63
|
-
type_desc = next(words)
|
|
64
|
-
type_from_default = _get_type_name_from_default(default_str)
|
|
65
|
-
desc = " ".join(words)
|
|
66
|
-
if type_desc[1:].islower() and "=" not in type_desc:
|
|
67
|
-
desc = f"{type_desc} {desc}"
|
|
68
|
-
type_desc = type_from_default
|
|
69
|
-
|
|
70
|
-
type_ = _get_type(type_from_default if default_str else type_desc)
|
|
71
|
-
return Argument(arg, type_desc, type_, desc, default_str)
|
|
72
|
-
|
|
73
|
-
def __eq__(self, other) -> bool:
|
|
74
|
-
if not isinstance(other, type(self)):
|
|
75
|
-
return False
|
|
76
|
-
return self.arg == other.arg
|
|
77
|
-
|
|
78
|
-
def __hash__(self):
|
|
79
|
-
return hash(self.arg)
|
|
67
|
+
raise ValueError(line)
|
|
80
68
|
|
|
81
69
|
|
|
82
70
|
def _collect_arguments(arguments: Iterable[str]) -> Iterator[str]:
|
|
@@ -91,7 +79,7 @@ def _collect_arguments(arguments: Iterable[str]) -> Iterator[str]:
|
|
|
91
79
|
yield res.strip()
|
|
92
80
|
|
|
93
81
|
|
|
94
|
-
def parse_dc_argument(lines:
|
|
82
|
+
def parse_dc_argument(lines: list[str]) -> list[Argument]:
|
|
95
83
|
"""
|
|
96
84
|
Parse arguments from lines of docker-compose specifications
|
|
97
85
|
:param lines: Lines of the Options sections from `docker-compose --help`.
|
|
@@ -101,18 +89,18 @@ def parse_dc_argument(lines: List[str]) -> List[Argument]:
|
|
|
101
89
|
return [Argument.from_line(line) for line in iter_lines if "--version" not in line]
|
|
102
90
|
|
|
103
91
|
|
|
104
|
-
def _get_type(type_name) ->
|
|
92
|
+
def _get_type(type_name) -> type:
|
|
105
93
|
res = _TYPE_CONVERSIONS.get(type_name, None)
|
|
106
94
|
if res is None:
|
|
107
95
|
if "=" in type_name:
|
|
108
96
|
res = dict
|
|
109
97
|
else:
|
|
110
|
-
logger.warning("Unknown type
|
|
98
|
+
logger.warning("Unknown type %s, use str", type_name)
|
|
111
99
|
res = str
|
|
112
100
|
return res
|
|
113
101
|
|
|
114
102
|
|
|
115
|
-
def _parse_arg(arg: str) ->
|
|
103
|
+
def _parse_arg(arg: str) -> tuple[str, str, bool]:
|
|
116
104
|
while arg.startswith("-"):
|
|
117
105
|
arg = arg[1:]
|
|
118
106
|
# if argument ends with a comma, it is followed by an alias. (usually -f, --foo)
|
|
@@ -138,11 +126,7 @@ def _get_type_name_from_default(default: str) -> str:
|
|
|
138
126
|
elif default == "proto":
|
|
139
127
|
return "str"
|
|
140
128
|
else:
|
|
141
|
-
|
|
142
|
-
return eval(default).__class__.__name__
|
|
143
|
-
except NameError:
|
|
144
|
-
logger.warning("Could not get type for value '{}'", default)
|
|
145
|
-
return "str"
|
|
129
|
+
raise NotImplementedError(default)
|
|
146
130
|
|
|
147
131
|
|
|
148
132
|
def _from_line_has_sep(line) -> "Argument":
|
|
@@ -153,17 +137,18 @@ def _from_line_has_sep(line) -> "Argument":
|
|
|
153
137
|
:param line: a single line with the description separated from the definition by at least two blanks
|
|
154
138
|
:return: Argument
|
|
155
139
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
min_arg_chars = 2 # Simple argument with single dash (e.g. -x)
|
|
141
|
+
min_full_chars = 4 # Named argument with double-dash (e.g. --xy)
|
|
142
|
+
desc_idx = line[min_arg_chars:].index(" ")
|
|
143
|
+
# Help output can wrap differently depending on terminal width.
|
|
144
|
+
# Normalize all whitespace so wrapped descriptions are stable.
|
|
145
|
+
desc = line[desc_idx + min_full_chars :].strip()
|
|
146
|
+
args = iter(line[: desc_idx + min_full_chars].split())
|
|
159
147
|
arg, default, has_more = "", "", True
|
|
160
148
|
while has_more:
|
|
161
149
|
arg, default, has_more = _parse_arg(next(args))
|
|
162
150
|
type_from_default = _get_type_name_from_default(default)
|
|
163
|
-
|
|
164
|
-
type_desc = next(args)
|
|
165
|
-
except StopIteration:
|
|
166
|
-
type_desc = type_from_default
|
|
151
|
+
type_desc = next(args, type_from_default)
|
|
167
152
|
|
|
168
153
|
type_ = _get_type(type_from_default if default else type_desc)
|
|
169
154
|
return Argument(arg, type_desc, type_, desc, default=default)
|
{docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/_utils/generate_class.py
RENAMED
|
@@ -3,54 +3,64 @@ from collections import defaultdict
|
|
|
3
3
|
from functools import lru_cache, reduce
|
|
4
4
|
from operator import add
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Iterator, Mapping, Union
|
|
7
7
|
|
|
8
8
|
import black
|
|
9
9
|
import isort
|
|
10
10
|
from isort.exceptions import ISortError
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
from docker_composer._utils.argument import Argument, parse_dc_argument
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# must be larger than 50 (otherwise it's ignored by docker compose)
|
|
19
|
+
_DEFAULT_HELP_COLUMNS = 120
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
@lru_cache()
|
|
17
23
|
def project_root():
|
|
18
24
|
for path in Path(__file__).parents:
|
|
19
25
|
if "pyproject.toml" in (p.name for p in path.iterdir()):
|
|
20
|
-
logger.debug("Project Path root:
|
|
26
|
+
logger.debug("Project Path root: %s", path)
|
|
21
27
|
return path
|
|
22
28
|
raise EnvironmentError("No pyproject.toml found in path hierarchy")
|
|
23
29
|
|
|
24
30
|
|
|
25
|
-
@lru_cache(
|
|
26
|
-
def _version(
|
|
27
|
-
return (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.decode("UTF-8")
|
|
31
|
-
)
|
|
31
|
+
@lru_cache()
|
|
32
|
+
def _version() -> str:
|
|
33
|
+
return subprocess.run(
|
|
34
|
+
("docker", "compose", "version"), capture_output=True, text=True
|
|
35
|
+
).stdout.strip()
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
@lru_cache()
|
|
35
39
|
def get_help_message(subcommand: str = "") -> str:
|
|
36
40
|
"""Obtain the help message for subcommand from docker-compose."""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
41
|
+
cmd = " ".join(arg for arg in ["docker", "compose", subcommand, "--help"] if arg)
|
|
42
|
+
full_cmd = f"stty cols {_DEFAULT_HELP_COLUMNS}; {cmd}"
|
|
43
|
+
try:
|
|
44
|
+
process = subprocess.run(
|
|
45
|
+
["script", "-q", "-c", full_cmd, "/dev/null"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
42
48
|
)
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
except Exception:
|
|
50
|
+
logger.error("FAILED to run %s.", full_cmd)
|
|
51
|
+
raise
|
|
52
|
+
if process.returncode:
|
|
53
|
+
logger.error("%s exited with %s:", cmd, process.returncode)
|
|
54
|
+
raise RuntimeError(process.stderr)
|
|
45
55
|
return process.stdout
|
|
46
56
|
|
|
47
57
|
|
|
48
|
-
def collect_help_lines(msg: str) -> Mapping[str,
|
|
58
|
+
def collect_help_lines(msg: str) -> Mapping[str, list[str]]:
|
|
49
59
|
"""Collect help messages into sections.
|
|
50
60
|
:param msg: Output from docker-compose <cmd> --help
|
|
51
61
|
:returns Mapping from section header to lines as lists. First (unnamed) section gets name "general"
|
|
52
62
|
"""
|
|
53
|
-
parts: Mapping[str,
|
|
63
|
+
parts: Mapping[str, list[str]] = defaultdict(list)
|
|
54
64
|
part = "general"
|
|
55
65
|
for line in msg.split("\n"):
|
|
56
66
|
if not line:
|
|
@@ -62,19 +72,14 @@ def collect_help_lines(msg: str) -> Mapping[str, List[str]]:
|
|
|
62
72
|
return parts
|
|
63
73
|
|
|
64
74
|
|
|
65
|
-
def parse_help(msg: str) ->
|
|
75
|
+
def parse_help(msg: str) -> tuple[Mapping[str, list[str]], list[Argument]]:
|
|
66
76
|
"""Helper function, get sections and arguments from docker-compose <cmd> --help text"""
|
|
67
77
|
sections = collect_help_lines(msg)
|
|
68
78
|
arguments = parse_dc_argument(sections["options"])
|
|
69
79
|
return sections, arguments
|
|
70
80
|
|
|
71
81
|
|
|
72
|
-
def
|
|
73
|
-
"""Flatten an iterable of lists"""
|
|
74
|
-
return reduce(add, lists, [])
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def indent(lines: Union[str, List[str]], level: int = 4) -> str:
|
|
82
|
+
def indent(lines: Union[str, list[str]], level: int = 4) -> str:
|
|
78
83
|
"""Indent lines by `level`.
|
|
79
84
|
:param lines: List of strings or a single string. A single string is splt up into individual lines.
|
|
80
85
|
:param level: number of spaces to indent
|
|
@@ -82,7 +87,7 @@ def indent(lines: Union[str, List[str]], level: int = 4) -> str:
|
|
|
82
87
|
"""
|
|
83
88
|
if isinstance(lines, str):
|
|
84
89
|
lines = lines.split("\n")
|
|
85
|
-
lines =
|
|
90
|
+
lines = reduce(add, (line.split("\n") for line in lines), [])
|
|
86
91
|
prefix = " " * level
|
|
87
92
|
if lines:
|
|
88
93
|
return prefix + f"\n{prefix}".join(lines)
|
|
@@ -90,26 +95,26 @@ def indent(lines: Union[str, List[str]], level: int = 4) -> str:
|
|
|
90
95
|
return ""
|
|
91
96
|
|
|
92
97
|
|
|
93
|
-
def get_docstring(sections: Mapping[str,
|
|
98
|
+
def get_docstring(sections: Mapping[str, list[str]]) -> list[str]:
|
|
94
99
|
"""Get (unindeted) docstring from docker-compose <cmd> --help. Use general and usage section.
|
|
95
100
|
:param sections: Output from `collect_help_lines`
|
|
96
101
|
"""
|
|
97
102
|
lines = sections["general"]
|
|
98
103
|
usages = sections.get("usage", [])
|
|
99
104
|
if usages:
|
|
100
|
-
lines
|
|
105
|
+
return [*lines, "Usage:", *usages]
|
|
101
106
|
return lines
|
|
102
107
|
|
|
103
108
|
|
|
104
|
-
def type_arg(arg: Argument) ->
|
|
109
|
+
def type_arg(arg: Argument) -> tuple[str, str]:
|
|
105
110
|
"""Generate the argument for docker-compose as a string, together with the doc-string"""
|
|
106
111
|
type_str = f"Optional[{arg.type_str}]"
|
|
107
112
|
return f"{arg.arg}: {type_str} = None", f'"""{arg.description}"""'
|
|
108
113
|
|
|
109
114
|
|
|
110
115
|
def get_def_commands(
|
|
111
|
-
sections: Mapping[str,
|
|
112
|
-
) -> Iterator[
|
|
116
|
+
sections: Mapping[str, list[str]], level: int = 0
|
|
117
|
+
) -> Iterator[tuple[str, list[str]]]:
|
|
113
118
|
"""Generate command functions as string"""
|
|
114
119
|
commands = sections.get("commands", None)
|
|
115
120
|
if not commands:
|
|
@@ -118,7 +123,7 @@ def get_def_commands(
|
|
|
118
123
|
if not line.strip():
|
|
119
124
|
continue
|
|
120
125
|
cmd = line.split()[0]
|
|
121
|
-
logger.debug("Generate run for
|
|
126
|
+
logger.debug("Generate run for %s", cmd)
|
|
122
127
|
docker_lines = get_help_message(cmd)
|
|
123
128
|
nl = level + 4
|
|
124
129
|
sections, arguments = parse_help(docker_lines)
|
|
@@ -147,47 +152,47 @@ def {cmd}(self, {args}) -> {class_name}:
|
|
|
147
152
|
)
|
|
148
153
|
|
|
149
154
|
|
|
150
|
-
def generate_class(
|
|
155
|
+
def generate_class(cmd: str) -> str:
|
|
156
|
+
class_name = f"DockerCompose{(cmd or 'Root').capitalize()}"
|
|
151
157
|
docker_lines = get_help_message(cmd)
|
|
152
|
-
nl =
|
|
158
|
+
nl = 4
|
|
153
159
|
sections, arguments = parse_help(docker_lines)
|
|
154
160
|
_add_custom_arguments(cmd, arguments)
|
|
155
161
|
cmd_fns = ""
|
|
156
|
-
add_imports:
|
|
162
|
+
add_imports: set[str] = set()
|
|
157
163
|
if "commands" in sections:
|
|
158
|
-
logger.info("Found commands in section
|
|
164
|
+
logger.info("Found commands in section %s", cmd)
|
|
159
165
|
for cmd_fn, add_import in get_def_commands(sections):
|
|
160
166
|
cmd_fns += cmd_fn
|
|
161
167
|
add_imports = add_imports.union(add_import)
|
|
162
|
-
args:
|
|
168
|
+
args: list[str] = reduce(add, (list(type_arg(arg)) for arg in arguments), [])
|
|
163
169
|
|
|
164
170
|
# List of argument that are options only
|
|
165
171
|
options = ", ".join([f'"{arg.arg}"' for arg in arguments if arg.is_option])
|
|
166
172
|
if options:
|
|
167
173
|
options = options + ","
|
|
168
|
-
new_line = "\n"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# DO NOT EDIT: Autogenerated by {__file__}
|
|
172
|
-
# for {_version(
|
|
173
|
-
|
|
174
|
-
import
|
|
175
|
-
from typing import Optional
|
|
174
|
+
new_line = "\n" # Python 3.9 does not support backslashes in format-strings.
|
|
175
|
+
|
|
176
|
+
res = f'''
|
|
177
|
+
# DO NOT EDIT: Autogenerated by {"/".join(Path(__file__).parts[-3:])}
|
|
178
|
+
# for {_version()}
|
|
179
|
+
|
|
180
|
+
import dataclasses as _dc
|
|
181
|
+
from typing import Optional
|
|
182
|
+
|
|
176
183
|
from docker_composer.base import DockerBaseRunner
|
|
177
184
|
{new_line.join(add_imports)}
|
|
178
185
|
|
|
179
|
-
@
|
|
186
|
+
@_dc.dataclass()
|
|
180
187
|
class {class_name}(DockerBaseRunner):
|
|
181
188
|
"""
|
|
182
189
|
{indent(get_docstring(sections), level=nl)}
|
|
183
190
|
"""
|
|
184
191
|
{indent(args, level=nl)}
|
|
185
|
-
_cmd: str = "{cmd or ""}"
|
|
186
|
-
_options:
|
|
192
|
+
_cmd: str = _dc.field(default="{cmd or ""}", repr=False, init=False)
|
|
193
|
+
_options: list[str] = _dc.field(default_factory=lambda: [{options}], repr=False, init=False)
|
|
187
194
|
{indent(cmd_fns, level=nl)}
|
|
188
|
-
'''
|
|
189
|
-
level=level,
|
|
190
|
-
)
|
|
195
|
+
'''
|
|
191
196
|
try:
|
|
192
197
|
res = isort.code(
|
|
193
198
|
res, config=isort.Config(settings_path=project_root().as_posix())
|
|
@@ -201,7 +206,7 @@ class {class_name}(DockerBaseRunner):
|
|
|
201
206
|
return res
|
|
202
207
|
|
|
203
208
|
|
|
204
|
-
def write_class(cmd: str) -> None:
|
|
209
|
+
def write_class(cmd: str = "") -> None:
|
|
205
210
|
"""
|
|
206
211
|
Generate a class for `cmd` and write it to `file_name`
|
|
207
212
|
|
|
@@ -210,17 +215,14 @@ def write_class(cmd: str) -> None:
|
|
|
210
215
|
:return:
|
|
211
216
|
"""
|
|
212
217
|
base_path = Path(__file__).parents[1] / "runner"
|
|
213
|
-
if cmd
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
file_name = base_path / "root.py"
|
|
217
|
-
|
|
218
|
-
class_str = generate_class(f"DockerCompose{(cmd or 'Root').capitalize()}", cmd)
|
|
219
|
-
logger.info("Write {:<8s} -> {}", cmd, file_name)
|
|
218
|
+
file_name = (base_path / "cmd" / f"{cmd}.py") if cmd else (base_path / "root.py")
|
|
219
|
+
class_str = generate_class(cmd)
|
|
220
|
+
logger.info("Write %s -> %s", cmd, file_name)
|
|
220
221
|
file_name.write_text(class_str, encoding="utf-8")
|
|
221
222
|
|
|
222
223
|
|
|
223
|
-
def _add_custom_arguments(cmd: str, arguments: list[Argument]):
|
|
224
|
+
def _add_custom_arguments(cmd: str, arguments: list[Argument]) -> None:
|
|
225
|
+
"""Add the verbose option to arguments."""
|
|
224
226
|
if cmd == "":
|
|
225
227
|
verbose = Argument("verbose", "OPTION", bool, "Use verbose output")
|
|
226
228
|
if verbose not in arguments:
|
|
@@ -228,8 +230,8 @@ def _add_custom_arguments(cmd: str, arguments: list[Argument]):
|
|
|
228
230
|
|
|
229
231
|
|
|
230
232
|
def main() -> None:
|
|
231
|
-
write_class(
|
|
232
|
-
docker_lines = get_help_message(
|
|
233
|
+
write_class()
|
|
234
|
+
docker_lines = get_help_message()
|
|
233
235
|
sections, _ = parse_help(docker_lines)
|
|
234
236
|
for cmd_line in sections["commands"]:
|
|
235
237
|
if not cmd_line:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def docker_compose_version_short() -> str:
|
|
7
|
+
return subprocess.check_output(
|
|
8
|
+
["docker", "compose", "version", "--short"],
|
|
9
|
+
text=True,
|
|
10
|
+
).strip()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
version = docker_compose_version_short()
|
|
15
|
+
if not version:
|
|
16
|
+
raise RuntimeError("docker compose version --short returned an empty version")
|
|
17
|
+
subprocess.check_call(["uv", "version", version])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main() # pragma: no cover
|