omdev 0.0.0.dev209__py3-none-any.whl → 0.0.0.dev211__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.
- omdev/.manifests.json +1 -1
- omdev/amalg/main.py +10 -1
- omdev/cc/cdeps.py +38 -0
- omdev/cc/cdeps.toml +4 -1
- omdev/cc/cli.py +2 -25
- omdev/ci/__init__.py +0 -0
- omdev/ci/__main__.py +4 -0
- omdev/ci/cache.py +168 -0
- omdev/ci/ci.py +249 -0
- omdev/ci/cli.py +189 -0
- omdev/ci/compose.py +214 -0
- omdev/ci/docker.py +151 -0
- omdev/ci/github/__init__.py +0 -0
- omdev/ci/github/bootstrap.py +11 -0
- omdev/ci/github/cache.py +355 -0
- omdev/ci/github/cacheapi.py +207 -0
- omdev/ci/github/cli.py +39 -0
- omdev/ci/requirements.py +80 -0
- omdev/ci/shell.py +42 -0
- omdev/ci/utils.py +81 -0
- omdev/interp/cli.py +0 -1
- omdev/interp/providers/system.py +2 -1
- omdev/pycharm/cli.py +1 -1
- omdev/pyproject/cli.py +0 -1
- omdev/revisions.py +0 -1
- omdev/scripts/ci.py +3435 -0
- omdev/scripts/interp.py +41 -25
- omdev/scripts/pyproject.py +41 -25
- omdev/tools/pawk/pawk.py +0 -1
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev211.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev211.dist-info}/RECORD +35 -18
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev211.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev211.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev211.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev211.dist-info}/top_level.txt +0 -0
omdev/ci/cli.py
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
# @omlish-amalg ../scripts/ci.py
|
2
|
+
# ruff: noqa: UP006 UP007
|
3
|
+
# @omlish-lite
|
4
|
+
"""
|
5
|
+
Inputs:
|
6
|
+
- requirements.txt
|
7
|
+
- ci.Dockerfile
|
8
|
+
- compose.yml
|
9
|
+
|
10
|
+
==
|
11
|
+
|
12
|
+
./python -m ci run --cache-dir ci/cache ci/project omlish-ci
|
13
|
+
"""
|
14
|
+
import asyncio
|
15
|
+
import os.path
|
16
|
+
import sys
|
17
|
+
import typing as ta
|
18
|
+
|
19
|
+
from omlish.argparse.cli import ArgparseCli
|
20
|
+
from omlish.argparse.cli import argparse_arg
|
21
|
+
from omlish.argparse.cli import argparse_cmd
|
22
|
+
from omlish.lite.check import check
|
23
|
+
from omlish.logs.standard import configure_standard_logging
|
24
|
+
|
25
|
+
from .cache import DirectoryFileCache
|
26
|
+
from .cache import DirectoryShellCache
|
27
|
+
from .cache import FileCache
|
28
|
+
from .cache import ShellCache
|
29
|
+
from .ci import Ci
|
30
|
+
from .compose import get_compose_service_dependencies
|
31
|
+
from .github.cache import GithubShellCache
|
32
|
+
from .github.cli import GithubCli
|
33
|
+
from .requirements import build_requirements_hash
|
34
|
+
from .shell import ShellCmd
|
35
|
+
|
36
|
+
|
37
|
+
class CiCli(ArgparseCli):
|
38
|
+
#
|
39
|
+
|
40
|
+
@argparse_cmd(
|
41
|
+
argparse_arg('requirements-txt', nargs='+'),
|
42
|
+
)
|
43
|
+
def print_requirements_hash(self) -> None:
|
44
|
+
requirements_txts = self.args.requirements_txt
|
45
|
+
|
46
|
+
print(build_requirements_hash(requirements_txts))
|
47
|
+
|
48
|
+
#
|
49
|
+
|
50
|
+
@argparse_cmd(
|
51
|
+
argparse_arg('compose-file'),
|
52
|
+
argparse_arg('service'),
|
53
|
+
)
|
54
|
+
def dump_compose_deps(self) -> None:
|
55
|
+
compose_file = self.args.compose_file
|
56
|
+
service = self.args.service
|
57
|
+
|
58
|
+
print(get_compose_service_dependencies(
|
59
|
+
compose_file,
|
60
|
+
service,
|
61
|
+
))
|
62
|
+
|
63
|
+
#
|
64
|
+
|
65
|
+
@argparse_cmd(
|
66
|
+
accepts_unknown=True,
|
67
|
+
)
|
68
|
+
def github(self) -> ta.Optional[int]:
|
69
|
+
return GithubCli(self.unknown_args).cli_run()
|
70
|
+
|
71
|
+
#
|
72
|
+
|
73
|
+
@argparse_cmd(
|
74
|
+
argparse_arg('project-dir'),
|
75
|
+
argparse_arg('service'),
|
76
|
+
argparse_arg('--docker-file'),
|
77
|
+
argparse_arg('--compose-file'),
|
78
|
+
argparse_arg('-r', '--requirements-txt', action='append'),
|
79
|
+
argparse_arg('--github-cache', action='store_true'),
|
80
|
+
argparse_arg('--cache-dir'),
|
81
|
+
argparse_arg('--always-pull', action='store_true'),
|
82
|
+
)
|
83
|
+
async def run(self) -> None:
|
84
|
+
project_dir = self.args.project_dir
|
85
|
+
docker_file = self.args.docker_file
|
86
|
+
compose_file = self.args.compose_file
|
87
|
+
service = self.args.service
|
88
|
+
requirements_txts = self.args.requirements_txt
|
89
|
+
cache_dir = self.args.cache_dir
|
90
|
+
always_pull = self.args.always_pull
|
91
|
+
|
92
|
+
#
|
93
|
+
|
94
|
+
check.state(os.path.isdir(project_dir))
|
95
|
+
|
96
|
+
#
|
97
|
+
|
98
|
+
def find_alt_file(*alts: str) -> ta.Optional[str]:
|
99
|
+
for alt in alts:
|
100
|
+
alt_file = os.path.abspath(os.path.join(project_dir, alt))
|
101
|
+
if os.path.isfile(alt_file):
|
102
|
+
return alt_file
|
103
|
+
return None
|
104
|
+
|
105
|
+
if docker_file is None:
|
106
|
+
docker_file = find_alt_file(
|
107
|
+
'docker/ci/Dockerfile',
|
108
|
+
'docker/ci.Dockerfile',
|
109
|
+
'ci.Dockerfile',
|
110
|
+
'Dockerfile',
|
111
|
+
)
|
112
|
+
check.state(os.path.isfile(docker_file))
|
113
|
+
|
114
|
+
if compose_file is None:
|
115
|
+
compose_file = find_alt_file(
|
116
|
+
'docker/compose.yml',
|
117
|
+
'compose.yml',
|
118
|
+
)
|
119
|
+
check.state(os.path.isfile(compose_file))
|
120
|
+
|
121
|
+
if not requirements_txts:
|
122
|
+
requirements_txts = []
|
123
|
+
for rf in [
|
124
|
+
'requirements.txt',
|
125
|
+
'requirements-dev.txt',
|
126
|
+
'requirements-ci.txt',
|
127
|
+
]:
|
128
|
+
if os.path.exists(os.path.join(project_dir, rf)):
|
129
|
+
requirements_txts.append(rf)
|
130
|
+
else:
|
131
|
+
for rf in requirements_txts:
|
132
|
+
check.state(os.path.isfile(rf))
|
133
|
+
|
134
|
+
#
|
135
|
+
|
136
|
+
shell_cache: ta.Optional[ShellCache] = None
|
137
|
+
file_cache: ta.Optional[FileCache] = None
|
138
|
+
if cache_dir is not None:
|
139
|
+
if not os.path.exists(cache_dir):
|
140
|
+
os.makedirs(cache_dir)
|
141
|
+
check.state(os.path.isdir(cache_dir))
|
142
|
+
|
143
|
+
directory_file_cache = DirectoryFileCache(cache_dir)
|
144
|
+
|
145
|
+
file_cache = directory_file_cache
|
146
|
+
|
147
|
+
if self.args.github_cache:
|
148
|
+
shell_cache = GithubShellCache(cache_dir)
|
149
|
+
else:
|
150
|
+
shell_cache = DirectoryShellCache(directory_file_cache)
|
151
|
+
|
152
|
+
#
|
153
|
+
|
154
|
+
with Ci(
|
155
|
+
Ci.Config(
|
156
|
+
project_dir=project_dir,
|
157
|
+
|
158
|
+
docker_file=docker_file,
|
159
|
+
|
160
|
+
compose_file=compose_file,
|
161
|
+
service=service,
|
162
|
+
|
163
|
+
requirements_txts=requirements_txts,
|
164
|
+
|
165
|
+
cmd=ShellCmd(' && '.join([
|
166
|
+
'cd /project',
|
167
|
+
'python3 -m pytest -svv test.py',
|
168
|
+
])),
|
169
|
+
|
170
|
+
always_pull=always_pull,
|
171
|
+
),
|
172
|
+
file_cache=file_cache,
|
173
|
+
shell_cache=shell_cache,
|
174
|
+
) as ci:
|
175
|
+
ci.run()
|
176
|
+
|
177
|
+
|
178
|
+
async def _async_main() -> ta.Optional[int]:
|
179
|
+
return await CiCli().async_cli_run()
|
180
|
+
|
181
|
+
|
182
|
+
def _main() -> None:
|
183
|
+
configure_standard_logging('DEBUG')
|
184
|
+
|
185
|
+
sys.exit(rc if isinstance(rc := asyncio.run(_async_main()), int) else 0)
|
186
|
+
|
187
|
+
|
188
|
+
if __name__ == '__main__':
|
189
|
+
_main()
|
omdev/ci/compose.py
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
TODO:
|
5
|
+
- fix rmi - only when not referenced anymore
|
6
|
+
"""
|
7
|
+
import contextlib
|
8
|
+
import dataclasses as dc
|
9
|
+
import itertools
|
10
|
+
import os.path
|
11
|
+
import shlex
|
12
|
+
import typing as ta
|
13
|
+
|
14
|
+
from omlish.lite.cached import cached_nullary
|
15
|
+
from omlish.lite.check import check
|
16
|
+
from omlish.lite.contextmanagers import ExitStacked
|
17
|
+
from omlish.lite.contextmanagers import defer
|
18
|
+
from omlish.lite.json import json_dumps_pretty
|
19
|
+
from omlish.subprocesses import subprocesses
|
20
|
+
|
21
|
+
from .shell import ShellCmd
|
22
|
+
from .utils import make_temp_file
|
23
|
+
from .utils import read_yaml_file
|
24
|
+
|
25
|
+
|
26
|
+
##
|
27
|
+
|
28
|
+
|
29
|
+
def get_compose_service_dependencies(
|
30
|
+
compose_file: str,
|
31
|
+
service: str,
|
32
|
+
) -> ta.Dict[str, str]:
|
33
|
+
compose_dct = read_yaml_file(compose_file)
|
34
|
+
|
35
|
+
services = compose_dct['services']
|
36
|
+
service_dct = services[service]
|
37
|
+
|
38
|
+
out = {}
|
39
|
+
for dep_service in service_dct.get('depends_on', []):
|
40
|
+
dep_service_dct = services[dep_service]
|
41
|
+
out[dep_service] = dep_service_dct['image']
|
42
|
+
|
43
|
+
return out
|
44
|
+
|
45
|
+
|
46
|
+
##
|
47
|
+
|
48
|
+
|
49
|
+
class DockerComposeRun(ExitStacked):
|
50
|
+
@dc.dataclass(frozen=True)
|
51
|
+
class Config:
|
52
|
+
compose_file: str
|
53
|
+
service: str
|
54
|
+
|
55
|
+
image: str
|
56
|
+
|
57
|
+
cmd: ShellCmd
|
58
|
+
|
59
|
+
#
|
60
|
+
|
61
|
+
run_options: ta.Optional[ta.Sequence[str]] = None
|
62
|
+
|
63
|
+
cwd: ta.Optional[str] = None
|
64
|
+
|
65
|
+
#
|
66
|
+
|
67
|
+
no_dependency_cleanup: bool = False
|
68
|
+
|
69
|
+
#
|
70
|
+
|
71
|
+
def __post_init__(self) -> None:
|
72
|
+
check.not_isinstance(self.run_options, str)
|
73
|
+
|
74
|
+
def __init__(self, cfg: Config) -> None:
|
75
|
+
super().__init__()
|
76
|
+
|
77
|
+
self._cfg = cfg
|
78
|
+
|
79
|
+
self._subprocess_kwargs = {
|
80
|
+
**(dict(cwd=self._cfg.cwd) if self._cfg.cwd is not None else {}),
|
81
|
+
}
|
82
|
+
|
83
|
+
#
|
84
|
+
|
85
|
+
@property
|
86
|
+
def image_tag(self) -> str:
|
87
|
+
pfx = 'sha256:'
|
88
|
+
if (image := self._cfg.image).startswith(pfx):
|
89
|
+
image = image[len(pfx):]
|
90
|
+
|
91
|
+
return f'{self._cfg.service}:{image}'
|
92
|
+
|
93
|
+
@cached_nullary
|
94
|
+
def tag_image(self) -> str:
|
95
|
+
image_tag = self.image_tag
|
96
|
+
|
97
|
+
subprocesses.check_call(
|
98
|
+
'docker',
|
99
|
+
'tag',
|
100
|
+
self._cfg.image,
|
101
|
+
image_tag,
|
102
|
+
**self._subprocess_kwargs,
|
103
|
+
)
|
104
|
+
|
105
|
+
def delete_tag() -> None:
|
106
|
+
subprocesses.check_call(
|
107
|
+
'docker',
|
108
|
+
'rmi',
|
109
|
+
image_tag,
|
110
|
+
**self._subprocess_kwargs,
|
111
|
+
)
|
112
|
+
|
113
|
+
self._enter_context(defer(delete_tag)) # noqa
|
114
|
+
|
115
|
+
return image_tag
|
116
|
+
|
117
|
+
#
|
118
|
+
|
119
|
+
def _rewrite_compose_dct(self, in_dct: ta.Dict[str, ta.Any]) -> ta.Dict[str, ta.Any]:
|
120
|
+
out = dict(in_dct)
|
121
|
+
|
122
|
+
#
|
123
|
+
|
124
|
+
in_services = in_dct['services']
|
125
|
+
out['services'] = out_services = {}
|
126
|
+
|
127
|
+
#
|
128
|
+
|
129
|
+
in_service: dict = in_services[self._cfg.service]
|
130
|
+
out_services[self._cfg.service] = out_service = dict(in_service)
|
131
|
+
|
132
|
+
out_service['image'] = self.image_tag
|
133
|
+
|
134
|
+
for k in ['build', 'platform']:
|
135
|
+
if k in out_service:
|
136
|
+
del out_service[k]
|
137
|
+
|
138
|
+
out_service['links'] = [
|
139
|
+
f'{l}:{l}' if ':' not in l else l
|
140
|
+
for l in out_service.get('links', [])
|
141
|
+
]
|
142
|
+
|
143
|
+
#
|
144
|
+
|
145
|
+
depends_on = in_service.get('depends_on', [])
|
146
|
+
|
147
|
+
for dep_service, in_dep_service_dct in list(in_services.items()):
|
148
|
+
if dep_service not in depends_on:
|
149
|
+
continue
|
150
|
+
|
151
|
+
out_dep_service: dict = dict(in_dep_service_dct)
|
152
|
+
out_services[dep_service] = out_dep_service
|
153
|
+
|
154
|
+
out_dep_service['ports'] = []
|
155
|
+
|
156
|
+
#
|
157
|
+
|
158
|
+
return out
|
159
|
+
|
160
|
+
@cached_nullary
|
161
|
+
def rewrite_compose_file(self) -> str:
|
162
|
+
in_dct = read_yaml_file(self._cfg.compose_file)
|
163
|
+
|
164
|
+
out_dct = self._rewrite_compose_dct(in_dct)
|
165
|
+
|
166
|
+
#
|
167
|
+
|
168
|
+
out_compose_file = make_temp_file()
|
169
|
+
self._enter_context(defer(lambda: os.unlink(out_compose_file))) # noqa
|
170
|
+
|
171
|
+
compose_json = json_dumps_pretty(out_dct)
|
172
|
+
|
173
|
+
with open(out_compose_file, 'w') as f:
|
174
|
+
f.write(compose_json)
|
175
|
+
|
176
|
+
return out_compose_file
|
177
|
+
|
178
|
+
#
|
179
|
+
|
180
|
+
def _cleanup_dependencies(self) -> None:
|
181
|
+
subprocesses.check_call(
|
182
|
+
'docker',
|
183
|
+
'compose',
|
184
|
+
'-f', self.rewrite_compose_file(),
|
185
|
+
'down',
|
186
|
+
)
|
187
|
+
|
188
|
+
def run(self) -> None:
|
189
|
+
self.tag_image()
|
190
|
+
|
191
|
+
compose_file = self.rewrite_compose_file()
|
192
|
+
|
193
|
+
with contextlib.ExitStack() as es:
|
194
|
+
if not self._cfg.no_dependency_cleanup:
|
195
|
+
es.enter_context(defer(self._cleanup_dependencies)) # noqa
|
196
|
+
|
197
|
+
sh_cmd = ' '.join([
|
198
|
+
'docker',
|
199
|
+
'compose',
|
200
|
+
'-f', compose_file,
|
201
|
+
'run',
|
202
|
+
'--rm',
|
203
|
+
*itertools.chain.from_iterable(['-e', k] for k in (self._cfg.cmd.env or [])),
|
204
|
+
*(self._cfg.run_options or []),
|
205
|
+
self._cfg.service,
|
206
|
+
'sh', '-c', shlex.quote(self._cfg.cmd.s),
|
207
|
+
])
|
208
|
+
|
209
|
+
run_cmd = dc.replace(self._cfg.cmd, s=sh_cmd)
|
210
|
+
|
211
|
+
run_cmd.run(
|
212
|
+
subprocesses.check_call,
|
213
|
+
**self._subprocess_kwargs,
|
214
|
+
)
|
omdev/ci/docker.py
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
TODO:
|
5
|
+
- some less stupid Dockerfile hash
|
6
|
+
- doesn't change too much though
|
7
|
+
"""
|
8
|
+
import contextlib
|
9
|
+
import dataclasses as dc
|
10
|
+
import json
|
11
|
+
import os.path
|
12
|
+
import shlex
|
13
|
+
import tarfile
|
14
|
+
import typing as ta
|
15
|
+
|
16
|
+
from omlish.lite.check import check
|
17
|
+
from omlish.lite.contextmanagers import defer
|
18
|
+
from omlish.subprocesses import subprocesses
|
19
|
+
|
20
|
+
from .shell import ShellCmd
|
21
|
+
from .utils import make_temp_file
|
22
|
+
from .utils import sha256_str
|
23
|
+
|
24
|
+
|
25
|
+
##
|
26
|
+
|
27
|
+
|
28
|
+
def build_docker_file_hash(docker_file: str) -> str:
|
29
|
+
with open(docker_file) as f:
|
30
|
+
contents = f.read()
|
31
|
+
|
32
|
+
return sha256_str(contents)
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
38
|
+
def read_docker_tar_image_tag(tar_file: str) -> str:
|
39
|
+
with tarfile.open(tar_file) as tf:
|
40
|
+
with contextlib.closing(check.not_none(tf.extractfile('manifest.json'))) as mf:
|
41
|
+
m = mf.read()
|
42
|
+
|
43
|
+
manifests = json.loads(m.decode('utf-8'))
|
44
|
+
manifest = check.single(manifests)
|
45
|
+
tag = check.non_empty_str(check.single(manifest['RepoTags']))
|
46
|
+
return tag
|
47
|
+
|
48
|
+
|
49
|
+
def read_docker_tar_image_id(tar_file: str) -> str:
|
50
|
+
with tarfile.open(tar_file) as tf:
|
51
|
+
with contextlib.closing(check.not_none(tf.extractfile('index.json'))) as mf:
|
52
|
+
i = mf.read()
|
53
|
+
|
54
|
+
index = json.loads(i.decode('utf-8'))
|
55
|
+
manifest = check.single(index['manifests'])
|
56
|
+
image_id = check.non_empty_str(manifest['digest'])
|
57
|
+
return image_id
|
58
|
+
|
59
|
+
|
60
|
+
##
|
61
|
+
|
62
|
+
|
63
|
+
def is_docker_image_present(image: str) -> bool:
|
64
|
+
out = subprocesses.check_output(
|
65
|
+
'docker',
|
66
|
+
'images',
|
67
|
+
'--format', 'json',
|
68
|
+
image,
|
69
|
+
)
|
70
|
+
|
71
|
+
out_s = out.decode('utf-8').strip()
|
72
|
+
if not out_s:
|
73
|
+
return False
|
74
|
+
|
75
|
+
json.loads(out_s) # noqa
|
76
|
+
return True
|
77
|
+
|
78
|
+
|
79
|
+
def pull_docker_image(
|
80
|
+
image: str,
|
81
|
+
) -> None:
|
82
|
+
subprocesses.check_call(
|
83
|
+
'docker',
|
84
|
+
'pull',
|
85
|
+
image,
|
86
|
+
)
|
87
|
+
|
88
|
+
|
89
|
+
def build_docker_image(
|
90
|
+
docker_file: str,
|
91
|
+
*,
|
92
|
+
cwd: ta.Optional[str] = None,
|
93
|
+
) -> str:
|
94
|
+
id_file = make_temp_file()
|
95
|
+
with defer(lambda: os.unlink(id_file)):
|
96
|
+
subprocesses.check_call(
|
97
|
+
'docker',
|
98
|
+
'build',
|
99
|
+
'-f', os.path.abspath(docker_file),
|
100
|
+
'--iidfile', id_file,
|
101
|
+
'--squash',
|
102
|
+
'.',
|
103
|
+
**(dict(cwd=cwd) if cwd is not None else {}),
|
104
|
+
)
|
105
|
+
|
106
|
+
with open(id_file) as f:
|
107
|
+
image_id = check.single(f.read().strip().splitlines()).strip()
|
108
|
+
|
109
|
+
return image_id
|
110
|
+
|
111
|
+
|
112
|
+
##
|
113
|
+
|
114
|
+
|
115
|
+
def save_docker_tar_cmd(
|
116
|
+
image: str,
|
117
|
+
output_cmd: ShellCmd,
|
118
|
+
) -> None:
|
119
|
+
cmd = dc.replace(output_cmd, s=f'docker save {image} | {output_cmd.s}')
|
120
|
+
cmd.run(subprocesses.check_call)
|
121
|
+
|
122
|
+
|
123
|
+
def save_docker_tar(
|
124
|
+
image: str,
|
125
|
+
tar_file: str,
|
126
|
+
) -> None:
|
127
|
+
return save_docker_tar_cmd(
|
128
|
+
image,
|
129
|
+
ShellCmd(f'cat > {shlex.quote(tar_file)}'),
|
130
|
+
)
|
131
|
+
|
132
|
+
|
133
|
+
#
|
134
|
+
|
135
|
+
|
136
|
+
def load_docker_tar_cmd(
|
137
|
+
input_cmd: ShellCmd,
|
138
|
+
) -> str:
|
139
|
+
cmd = dc.replace(input_cmd, s=f'{input_cmd.s} | docker load')
|
140
|
+
|
141
|
+
out = cmd.run(subprocesses.check_output).decode()
|
142
|
+
|
143
|
+
line = check.single(out.strip().splitlines())
|
144
|
+
loaded = line.partition(':')[2].strip()
|
145
|
+
return loaded
|
146
|
+
|
147
|
+
|
148
|
+
def load_docker_tar(
|
149
|
+
tar_file: str,
|
150
|
+
) -> str:
|
151
|
+
return load_docker_tar_cmd(ShellCmd(f'cat {shlex.quote(tar_file)}'))
|
File without changes
|