omdev 0.0.0.dev211__py3-none-any.whl → 0.0.0.dev213__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/ci/ci.py CHANGED
@@ -7,9 +7,10 @@ import tarfile
7
7
  import tempfile
8
8
  import typing as ta
9
9
 
10
+ from omlish.lite.cached import async_cached_nullary
10
11
  from omlish.lite.cached import cached_nullary
11
12
  from omlish.lite.check import check
12
- from omlish.lite.contextmanagers import ExitStacked
13
+ from omlish.lite.contextmanagers import AsyncExitStacked
13
14
  from omlish.lite.contextmanagers import defer
14
15
 
15
16
  from .cache import FileCache
@@ -22,13 +23,14 @@ from .docker import is_docker_image_present
22
23
  from .docker import load_docker_tar_cmd
23
24
  from .docker import pull_docker_image
24
25
  from .docker import save_docker_tar_cmd
26
+ from .docker import tag_docker_image
25
27
  from .requirements import build_requirements_hash
26
28
  from .requirements import download_requirements
27
29
  from .shell import ShellCmd
28
30
  from .utils import log_timing_context
29
31
 
30
32
 
31
- class Ci(ExitStacked):
33
+ class Ci(AsyncExitStacked):
32
34
  FILE_NAME_HASH_LEN = 16
33
35
 
34
36
  @dc.dataclass(frozen=True)
@@ -45,6 +47,9 @@ class Ci(ExitStacked):
45
47
  requirements_txts: ta.Optional[ta.Sequence[str]] = None
46
48
 
47
49
  always_pull: bool = False
50
+ always_build: bool = False
51
+
52
+ no_dependencies: bool = False
48
53
 
49
54
  def __post_init__(self) -> None:
50
55
  check.not_isinstance(self.requirements_txts, str)
@@ -64,7 +69,7 @@ class Ci(ExitStacked):
64
69
 
65
70
  #
66
71
 
67
- def _load_cache_docker_image(self, key: str) -> ta.Optional[str]:
72
+ async def _load_cache_docker_image(self, key: str) -> ta.Optional[str]:
68
73
  if self._shell_cache is None:
69
74
  return None
70
75
 
@@ -74,9 +79,9 @@ class Ci(ExitStacked):
74
79
 
75
80
  get_cache_cmd = dc.replace(get_cache_cmd, s=f'{get_cache_cmd.s} | zstd -cd --long') # noqa
76
81
 
77
- return load_docker_tar_cmd(get_cache_cmd)
82
+ return await load_docker_tar_cmd(get_cache_cmd)
78
83
 
79
- def _save_cache_docker_image(self, key: str, image: str) -> None:
84
+ async def _save_cache_docker_image(self, key: str, image: str) -> None:
80
85
  if self._shell_cache is None:
81
86
  return
82
87
 
@@ -85,12 +90,12 @@ class Ci(ExitStacked):
85
90
 
86
91
  put_cache_cmd = dc.replace(put_cache_cmd, s=f'zstd | {put_cache_cmd.s}')
87
92
 
88
- save_docker_tar_cmd(image, put_cache_cmd)
93
+ await save_docker_tar_cmd(image, put_cache_cmd)
89
94
 
90
95
  #
91
96
 
92
- def _load_docker_image(self, image: str) -> None:
93
- if not self._cfg.always_pull and is_docker_image_present(image):
97
+ async def _load_docker_image(self, image: str) -> None:
98
+ if not self._cfg.always_pull and (await is_docker_image_present(image)):
94
99
  return
95
100
 
96
101
  dep_suffix = image
@@ -98,63 +103,79 @@ class Ci(ExitStacked):
98
103
  dep_suffix = dep_suffix.replace(c, '-')
99
104
 
100
105
  cache_key = f'docker-{dep_suffix}'
101
- if self._load_cache_docker_image(cache_key) is not None:
106
+ if (await self._load_cache_docker_image(cache_key)) is not None:
102
107
  return
103
108
 
104
- pull_docker_image(image)
109
+ await pull_docker_image(image)
105
110
 
106
- self._save_cache_docker_image(cache_key, image)
111
+ await self._save_cache_docker_image(cache_key, image)
107
112
 
108
- def load_docker_image(self, image: str) -> None:
113
+ async def load_docker_image(self, image: str) -> None:
109
114
  with log_timing_context(f'Load docker image: {image}'):
110
- self._load_docker_image(image)
115
+ await self._load_docker_image(image)
111
116
 
112
- @cached_nullary
113
- def load_compose_service_dependencies(self) -> None:
117
+ @async_cached_nullary
118
+ async def load_compose_service_dependencies(self) -> None:
114
119
  deps = get_compose_service_dependencies(
115
120
  self._cfg.compose_file,
116
121
  self._cfg.service,
117
122
  )
118
123
 
119
124
  for dep_image in deps.values():
120
- self.load_docker_image(dep_image)
125
+ await self.load_docker_image(dep_image)
121
126
 
122
127
  #
123
128
 
124
- def _resolve_ci_image(self) -> str:
125
- docker_file_hash = build_docker_file_hash(self._cfg.docker_file)[:self.FILE_NAME_HASH_LEN]
129
+ @cached_nullary
130
+ def docker_file_hash(self) -> str:
131
+ return build_docker_file_hash(self._cfg.docker_file)[:self.FILE_NAME_HASH_LEN]
132
+
133
+ async def _resolve_ci_image(self) -> str:
134
+ cache_key = f'ci-{self.docker_file_hash()}'
135
+ image_tag = f'{self._cfg.service}:{cache_key}'
136
+
137
+ if not self._cfg.always_build and (await is_docker_image_present(image_tag)):
138
+ return image_tag
126
139
 
127
- cache_key = f'ci-{docker_file_hash}'
128
- if (cache_image_id := self._load_cache_docker_image(cache_key)) is not None:
129
- return cache_image_id
140
+ if (cache_image_id := await self._load_cache_docker_image(cache_key)) is not None:
141
+ await tag_docker_image(
142
+ cache_image_id,
143
+ image_tag,
144
+ )
145
+ return image_tag
130
146
 
131
- image_id = build_docker_image(
147
+ image_id = await build_docker_image(
132
148
  self._cfg.docker_file,
149
+ tag=image_tag,
133
150
  cwd=self._cfg.project_dir,
134
151
  )
135
152
 
136
- self._save_cache_docker_image(cache_key, image_id)
153
+ await self._save_cache_docker_image(cache_key, image_id)
137
154
 
138
- return image_id
155
+ return image_tag
139
156
 
140
- @cached_nullary
141
- def resolve_ci_image(self) -> str:
157
+ @async_cached_nullary
158
+ async def resolve_ci_image(self) -> str:
142
159
  with log_timing_context('Resolve ci image') as ltc:
143
- image_id = self._resolve_ci_image()
160
+ image_id = await self._resolve_ci_image()
144
161
  ltc.set_description(f'Resolve ci image: {image_id}')
145
162
  return image_id
146
163
 
147
164
  #
148
165
 
149
- def _resolve_requirements_dir(self) -> str:
150
- requirements_txts = [
166
+ @cached_nullary
167
+ def requirements_txts(self) -> ta.Sequence[str]:
168
+ return [
151
169
  os.path.join(self._cfg.project_dir, rf)
152
170
  for rf in check.not_none(self._cfg.requirements_txts)
153
171
  ]
154
172
 
155
- requirements_hash = build_requirements_hash(requirements_txts)[:self.FILE_NAME_HASH_LEN]
173
+ @cached_nullary
174
+ def requirements_hash(self) -> str:
175
+ return build_requirements_hash(self.requirements_txts())[:self.FILE_NAME_HASH_LEN]
156
176
 
157
- tar_file_key = f'requirements-{requirements_hash}'
177
+ async def _resolve_requirements_dir(self) -> str:
178
+ tar_file_key = f'requirements-{self.docker_file_hash()}-{self.requirements_hash()}'
158
179
  tar_file_name = f'{tar_file_key}.tar'
159
180
 
160
181
  temp_dir = tempfile.mkdtemp()
@@ -170,9 +191,9 @@ class Ci(ExitStacked):
170
191
  os.makedirs(temp_requirements_dir)
171
192
 
172
193
  download_requirements(
173
- self.resolve_ci_image(),
194
+ await self.resolve_ci_image(),
174
195
  temp_requirements_dir,
175
- requirements_txts,
196
+ self.requirements_txts(),
176
197
  )
177
198
 
178
199
  if self._file_cache is not None:
@@ -189,16 +210,16 @@ class Ci(ExitStacked):
189
210
 
190
211
  return temp_requirements_dir
191
212
 
192
- @cached_nullary
193
- def resolve_requirements_dir(self) -> str:
213
+ @async_cached_nullary
214
+ async def resolve_requirements_dir(self) -> str:
194
215
  with log_timing_context('Resolve requirements dir') as ltc:
195
- requirements_dir = self._resolve_requirements_dir()
216
+ requirements_dir = await self._resolve_requirements_dir()
196
217
  ltc.set_description(f'Resolve requirements dir: {requirements_dir}')
197
218
  return requirements_dir
198
219
 
199
220
  #
200
221
 
201
- def _run_compose_(self) -> None:
222
+ async def _run_compose_(self) -> None:
202
223
  setup_cmds = [
203
224
  'pip install --root-user-action ignore --find-links /requirements --no-index uv',
204
225
  (
@@ -216,34 +237,36 @@ class Ci(ExitStacked):
216
237
 
217
238
  #
218
239
 
219
- with DockerComposeRun(DockerComposeRun.Config(
240
+ async with DockerComposeRun(DockerComposeRun.Config(
220
241
  compose_file=self._cfg.compose_file,
221
242
  service=self._cfg.service,
222
243
 
223
- image=self.resolve_ci_image(),
244
+ image=await self.resolve_ci_image(),
224
245
 
225
246
  cmd=ci_cmd,
226
247
 
227
248
  run_options=[
228
249
  '-v', f'{os.path.abspath(self._cfg.project_dir)}:/project',
229
- '-v', f'{os.path.abspath(self.resolve_requirements_dir())}:/requirements',
250
+ '-v', f'{os.path.abspath(await self.resolve_requirements_dir())}:/requirements',
230
251
  ],
231
252
 
232
253
  cwd=self._cfg.project_dir,
254
+
255
+ no_dependencies=self._cfg.no_dependencies,
233
256
  )) as ci_compose_run:
234
- ci_compose_run.run()
257
+ await ci_compose_run.run()
235
258
 
236
- def _run_compose(self) -> None:
259
+ async def _run_compose(self) -> None:
237
260
  with log_timing_context('Run compose'):
238
- self._run_compose_()
261
+ await self._run_compose_()
239
262
 
240
263
  #
241
264
 
242
- def run(self) -> None:
243
- self.load_compose_service_dependencies()
265
+ async def run(self) -> None:
266
+ await self.load_compose_service_dependencies()
244
267
 
245
- self.resolve_ci_image()
268
+ await self.resolve_ci_image()
246
269
 
247
- self.resolve_requirements_dir()
270
+ await self.resolve_requirements_dir()
248
271
 
249
- self._run_compose()
272
+ await self._run_compose()
omdev/ci/cli.py CHANGED
@@ -76,18 +76,21 @@ class CiCli(ArgparseCli):
76
76
  argparse_arg('--docker-file'),
77
77
  argparse_arg('--compose-file'),
78
78
  argparse_arg('-r', '--requirements-txt', action='append'),
79
+
79
80
  argparse_arg('--github-cache', action='store_true'),
80
81
  argparse_arg('--cache-dir'),
82
+
81
83
  argparse_arg('--always-pull', action='store_true'),
84
+ argparse_arg('--always-build', action='store_true'),
85
+
86
+ argparse_arg('--no-dependencies', action='store_true'),
82
87
  )
83
88
  async def run(self) -> None:
84
89
  project_dir = self.args.project_dir
85
90
  docker_file = self.args.docker_file
86
91
  compose_file = self.args.compose_file
87
- service = self.args.service
88
92
  requirements_txts = self.args.requirements_txt
89
93
  cache_dir = self.args.cache_dir
90
- always_pull = self.args.always_pull
91
94
 
92
95
  #
93
96
 
@@ -112,10 +115,16 @@ class CiCli(ArgparseCli):
112
115
  check.state(os.path.isfile(docker_file))
113
116
 
114
117
  if compose_file is None:
115
- compose_file = find_alt_file(
116
- 'docker/compose.yml',
117
- 'compose.yml',
118
- )
118
+ compose_file = find_alt_file(*[
119
+ f'{f}.{x}'
120
+ for f in [
121
+ 'docker/docker-compose',
122
+ 'docker/compose',
123
+ 'docker-compose',
124
+ 'compose',
125
+ ]
126
+ for x in ['yaml', 'yml']
127
+ ])
119
128
  check.state(os.path.isfile(compose_file))
120
129
 
121
130
  if not requirements_txts:
@@ -151,14 +160,14 @@ class CiCli(ArgparseCli):
151
160
 
152
161
  #
153
162
 
154
- with Ci(
163
+ async with Ci(
155
164
  Ci.Config(
156
165
  project_dir=project_dir,
157
166
 
158
167
  docker_file=docker_file,
159
168
 
160
169
  compose_file=compose_file,
161
- service=service,
170
+ service=self.args.service,
162
171
 
163
172
  requirements_txts=requirements_txts,
164
173
 
@@ -167,12 +176,15 @@ class CiCli(ArgparseCli):
167
176
  'python3 -m pytest -svv test.py',
168
177
  ])),
169
178
 
170
- always_pull=always_pull,
179
+ always_pull=self.args.always_pull,
180
+ always_build=self.args.always_build,
181
+
182
+ no_dependencies=self.args.no_dependencies,
171
183
  ),
172
184
  file_cache=file_cache,
173
185
  shell_cache=shell_cache,
174
186
  ) as ci:
175
- ci.run()
187
+ await ci.run()
176
188
 
177
189
 
178
190
  async def _async_main() -> ta.Optional[int]:
omdev/ci/compose.py CHANGED
@@ -11,12 +11,13 @@ import os.path
11
11
  import shlex
12
12
  import typing as ta
13
13
 
14
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
14
15
  from omlish.lite.cached import cached_nullary
15
16
  from omlish.lite.check import check
16
- from omlish.lite.contextmanagers import ExitStacked
17
+ from omlish.lite.contextmanagers import AsyncExitStacked
18
+ from omlish.lite.contextmanagers import adefer
17
19
  from omlish.lite.contextmanagers import defer
18
20
  from omlish.lite.json import json_dumps_pretty
19
- from omlish.subprocesses import subprocesses
20
21
 
21
22
  from .shell import ShellCmd
22
23
  from .utils import make_temp_file
@@ -46,7 +47,7 @@ def get_compose_service_dependencies(
46
47
  ##
47
48
 
48
49
 
49
- class DockerComposeRun(ExitStacked):
50
+ class DockerComposeRun(AsyncExitStacked):
50
51
  @dc.dataclass(frozen=True)
51
52
  class Config:
52
53
  compose_file: str
@@ -64,6 +65,7 @@ class DockerComposeRun(ExitStacked):
64
65
 
65
66
  #
66
67
 
68
+ no_dependencies: bool = False
67
69
  no_dependency_cleanup: bool = False
68
70
 
69
71
  #
@@ -82,40 +84,6 @@ class DockerComposeRun(ExitStacked):
82
84
 
83
85
  #
84
86
 
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
87
  def _rewrite_compose_dct(self, in_dct: ta.Dict[str, ta.Any]) -> ta.Dict[str, ta.Any]:
120
88
  out = dict(in_dct)
121
89
 
@@ -129,7 +97,7 @@ class DockerComposeRun(ExitStacked):
129
97
  in_service: dict = in_services[self._cfg.service]
130
98
  out_services[self._cfg.service] = out_service = dict(in_service)
131
99
 
132
- out_service['image'] = self.image_tag
100
+ out_service['image'] = self._cfg.image
133
101
 
134
102
  for k in ['build', 'platform']:
135
103
  if k in out_service:
@@ -142,16 +110,21 @@ class DockerComposeRun(ExitStacked):
142
110
 
143
111
  #
144
112
 
145
- depends_on = in_service.get('depends_on', [])
113
+ if not self._cfg.no_dependencies:
114
+ depends_on = in_service.get('depends_on', [])
146
115
 
147
- for dep_service, in_dep_service_dct in list(in_services.items()):
148
- if dep_service not in depends_on:
149
- continue
116
+ for dep_service, in_dep_service_dct in list(in_services.items()):
117
+ if dep_service not in depends_on:
118
+ continue
150
119
 
151
- out_dep_service: dict = dict(in_dep_service_dct)
152
- out_services[dep_service] = out_dep_service
120
+ out_dep_service: dict = dict(in_dep_service_dct)
121
+ out_services[dep_service] = out_dep_service
153
122
 
154
- out_dep_service['ports'] = []
123
+ out_dep_service['ports'] = []
124
+
125
+ else:
126
+ out_service['depends_on'] = []
127
+ out_service['links'] = []
155
128
 
156
129
  #
157
130
 
@@ -177,22 +150,20 @@ class DockerComposeRun(ExitStacked):
177
150
 
178
151
  #
179
152
 
180
- def _cleanup_dependencies(self) -> None:
181
- subprocesses.check_call(
153
+ async def _cleanup_dependencies(self) -> None:
154
+ await asyncio_subprocesses.check_call(
182
155
  'docker',
183
156
  'compose',
184
157
  '-f', self.rewrite_compose_file(),
185
158
  'down',
186
159
  )
187
160
 
188
- def run(self) -> None:
189
- self.tag_image()
190
-
161
+ async def run(self) -> None:
191
162
  compose_file = self.rewrite_compose_file()
192
163
 
193
- with contextlib.ExitStack() as es:
194
- if not self._cfg.no_dependency_cleanup:
195
- es.enter_context(defer(self._cleanup_dependencies)) # noqa
164
+ async with contextlib.AsyncExitStack() as es:
165
+ if not (self._cfg.no_dependencies or self._cfg.no_dependency_cleanup):
166
+ await es.enter_async_context(adefer(self._cleanup_dependencies)) # noqa
196
167
 
197
168
  sh_cmd = ' '.join([
198
169
  'docker',
@@ -200,7 +171,10 @@ class DockerComposeRun(ExitStacked):
200
171
  '-f', compose_file,
201
172
  'run',
202
173
  '--rm',
203
- *itertools.chain.from_iterable(['-e', k] for k in (self._cfg.cmd.env or [])),
174
+ *itertools.chain.from_iterable(
175
+ ['-e', k]
176
+ for k in (self._cfg.cmd.env or [])
177
+ ),
204
178
  *(self._cfg.run_options or []),
205
179
  self._cfg.service,
206
180
  'sh', '-c', shlex.quote(self._cfg.cmd.s),
@@ -208,7 +182,7 @@ class DockerComposeRun(ExitStacked):
208
182
 
209
183
  run_cmd = dc.replace(self._cfg.cmd, s=sh_cmd)
210
184
 
211
- run_cmd.run(
212
- subprocesses.check_call,
185
+ await run_cmd.run(
186
+ asyncio_subprocesses.check_call,
213
187
  **self._subprocess_kwargs,
214
188
  )
omdev/ci/docker.py CHANGED
@@ -13,9 +13,9 @@ import shlex
13
13
  import tarfile
14
14
  import typing as ta
15
15
 
16
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
16
17
  from omlish.lite.check import check
17
18
  from omlish.lite.contextmanagers import defer
18
- from omlish.subprocesses import subprocesses
19
19
 
20
20
  from .shell import ShellCmd
21
21
  from .utils import make_temp_file
@@ -60,8 +60,8 @@ def read_docker_tar_image_id(tar_file: str) -> str:
60
60
  ##
61
61
 
62
62
 
63
- def is_docker_image_present(image: str) -> bool:
64
- out = subprocesses.check_output(
63
+ async def is_docker_image_present(image: str) -> bool:
64
+ out = await asyncio_subprocesses.check_output(
65
65
  'docker',
66
66
  'images',
67
67
  '--format', 'json',
@@ -76,55 +76,74 @@ def is_docker_image_present(image: str) -> bool:
76
76
  return True
77
77
 
78
78
 
79
- def pull_docker_image(
79
+ async def pull_docker_image(
80
80
  image: str,
81
81
  ) -> None:
82
- subprocesses.check_call(
82
+ await asyncio_subprocesses.check_call(
83
83
  'docker',
84
84
  'pull',
85
85
  image,
86
86
  )
87
87
 
88
88
 
89
- def build_docker_image(
89
+ async def build_docker_image(
90
90
  docker_file: str,
91
91
  *,
92
+ tag: ta.Optional[str] = None,
92
93
  cwd: ta.Optional[str] = None,
93
94
  ) -> str:
94
95
  id_file = make_temp_file()
95
96
  with defer(lambda: os.unlink(id_file)):
96
- subprocesses.check_call(
97
+ await asyncio_subprocesses.check_call(
97
98
  'docker',
98
99
  'build',
99
100
  '-f', os.path.abspath(docker_file),
100
101
  '--iidfile', id_file,
101
102
  '--squash',
103
+ *(['--tag', tag] if tag is not None else []),
102
104
  '.',
103
105
  **(dict(cwd=cwd) if cwd is not None else {}),
104
106
  )
105
107
 
106
- with open(id_file) as f:
108
+ with open(id_file) as f: # noqa
107
109
  image_id = check.single(f.read().strip().splitlines()).strip()
108
110
 
109
111
  return image_id
110
112
 
111
113
 
114
+ async def tag_docker_image(image: str, tag: str) -> None:
115
+ await asyncio_subprocesses.check_call(
116
+ 'docker',
117
+ 'tag',
118
+ image,
119
+ tag,
120
+ )
121
+
122
+
123
+ async def delete_docker_tag(tag: str) -> None:
124
+ await asyncio_subprocesses.check_call(
125
+ 'docker',
126
+ 'rmi',
127
+ tag,
128
+ )
129
+
130
+
112
131
  ##
113
132
 
114
133
 
115
- def save_docker_tar_cmd(
134
+ async def save_docker_tar_cmd(
116
135
  image: str,
117
136
  output_cmd: ShellCmd,
118
137
  ) -> None:
119
138
  cmd = dc.replace(output_cmd, s=f'docker save {image} | {output_cmd.s}')
120
- cmd.run(subprocesses.check_call)
139
+ await cmd.run(asyncio_subprocesses.check_call)
121
140
 
122
141
 
123
- def save_docker_tar(
142
+ async def save_docker_tar(
124
143
  image: str,
125
144
  tar_file: str,
126
145
  ) -> None:
127
- return save_docker_tar_cmd(
146
+ return await save_docker_tar_cmd(
128
147
  image,
129
148
  ShellCmd(f'cat > {shlex.quote(tar_file)}'),
130
149
  )
@@ -133,19 +152,19 @@ def save_docker_tar(
133
152
  #
134
153
 
135
154
 
136
- def load_docker_tar_cmd(
155
+ async def load_docker_tar_cmd(
137
156
  input_cmd: ShellCmd,
138
157
  ) -> str:
139
158
  cmd = dc.replace(input_cmd, s=f'{input_cmd.s} | docker load')
140
159
 
141
- out = cmd.run(subprocesses.check_output).decode()
160
+ out = (await cmd.run(asyncio_subprocesses.check_output)).decode()
142
161
 
143
162
  line = check.single(out.strip().splitlines())
144
163
  loaded = line.partition(':')[2].strip()
145
164
  return loaded
146
165
 
147
166
 
148
- def load_docker_tar(
167
+ async def load_docker_tar(
149
168
  tar_file: str,
150
169
  ) -> str:
151
- return load_docker_tar_cmd(ShellCmd(f'cat {shlex.quote(tar_file)}'))
170
+ return await load_docker_tar_cmd(ShellCmd(f'cat {shlex.quote(tar_file)}'))