omdev 0.0.0.dev240__py3-none-any.whl → 0.0.0.dev241__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 CHANGED
@@ -267,7 +267,7 @@
267
267
  "module": ".tools.docker",
268
268
  "attr": "_CLI_MODULE",
269
269
  "file": "omdev/tools/docker.py",
270
- "line": 267,
270
+ "line": 251,
271
271
  "value": {
272
272
  "$.cli.types.CliModule": {
273
273
  "cmd_name": "docker",
omdev/ci/cache.py CHANGED
@@ -118,7 +118,7 @@ class DirectoryFileCache(FileCache):
118
118
  elif not os.path.isdir(self.dir):
119
119
  os.makedirs(self.dir)
120
120
  with open(version_file, 'w') as f:
121
- f.write(str(self._version))
121
+ f.write(f'{self._version}\n')
122
122
  return
123
123
 
124
124
  # NOTE: intentionally raises FileNotFoundError to refuse to use an existing non-cache dir as a cache dir.
omdev/ci/ci.py CHANGED
@@ -1,8 +1,10 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import dataclasses as dc
3
+ import functools
3
4
  import os.path
4
5
  import typing as ta
5
6
 
7
+ from omlish.asyncs.asyncio.asyncio import asyncio_wait_maybe_concurrent
6
8
  from omlish.lite.cached import async_cached_nullary
7
9
  from omlish.lite.cached import cached_nullary
8
10
  from omlish.lite.check import check
@@ -13,6 +15,7 @@ from omlish.os.temp import temp_file_context
13
15
  from .compose import DockerComposeRun
14
16
  from .compose import get_compose_service_dependencies
15
17
  from .docker.buildcaching import DockerBuildCaching
18
+ from .docker.cache import DockerCacheKey
16
19
  from .docker.cmds import build_docker_image
17
20
  from .docker.imagepulling import DockerImagePulling
18
21
  from .docker.utils import build_docker_file_hash
@@ -44,6 +47,8 @@ class Ci(AsyncExitStacked):
44
47
  always_pull: bool = False
45
48
  always_build: bool = False
46
49
 
50
+ setup_concurrency: ta.Optional[int] = None
51
+
47
52
  no_dependencies: bool = False
48
53
 
49
54
  run_options: ta.Optional[ta.Sequence[str]] = None
@@ -74,8 +79,8 @@ class Ci(AsyncExitStacked):
74
79
  return build_docker_file_hash(self._config.docker_file)[:self.KEY_HASH_LEN]
75
80
 
76
81
  @cached_nullary
77
- def ci_base_image_cache_key(self) -> str:
78
- return f'ci-base-{self.docker_file_hash()}'
82
+ def ci_base_image_cache_key(self) -> DockerCacheKey:
83
+ return DockerCacheKey(['ci-base'], self.docker_file_hash())
79
84
 
80
85
  async def _resolve_ci_base_image(self) -> str:
81
86
  async def build_and_tag(image_tag: str) -> str:
@@ -111,8 +116,8 @@ class Ci(AsyncExitStacked):
111
116
  return build_requirements_hash(self.requirements_txts())[:self.KEY_HASH_LEN]
112
117
 
113
118
  @cached_nullary
114
- def ci_image_cache_key(self) -> str:
115
- return f'ci-{self.docker_file_hash()}-{self.requirements_hash()}'
119
+ def ci_image_cache_key(self) -> DockerCacheKey:
120
+ return DockerCacheKey(['ci'], f'{self.docker_file_hash()}-{self.requirements_hash()}')
116
121
 
117
122
  async def _resolve_ci_image(self) -> str:
118
123
  async def build_and_tag(image_tag: str) -> str:
@@ -168,15 +173,37 @@ class Ci(AsyncExitStacked):
168
173
 
169
174
  #
170
175
 
171
- @async_cached_nullary
172
- async def pull_dependencies(self) -> None:
176
+ @cached_nullary
177
+ def pull_dependencies_funcs(self) -> ta.Sequence[ta.Callable[[], ta.Awaitable]]:
173
178
  deps = get_compose_service_dependencies(
174
179
  self._config.compose_file,
175
180
  self._config.service,
176
181
  )
177
182
 
178
- for dep_image in deps.values():
179
- await self._docker_image_pulling.pull_docker_image(dep_image)
183
+ return [
184
+ async_cached_nullary(functools.partial(
185
+ self._docker_image_pulling.pull_docker_image,
186
+ dep_image,
187
+ ))
188
+ for dep_image in deps.values()
189
+ ]
190
+
191
+ #
192
+
193
+ @cached_nullary
194
+ def setup_funcs(self) -> ta.Sequence[ta.Callable[[], ta.Awaitable]]:
195
+ return [
196
+ self.resolve_ci_image,
197
+
198
+ *(self.pull_dependencies_funcs() if not self._config.no_dependencies else []),
199
+ ]
200
+
201
+ @async_cached_nullary
202
+ async def setup(self) -> None:
203
+ await asyncio_wait_maybe_concurrent(
204
+ [fn() for fn in self.setup_funcs()],
205
+ self._config.setup_concurrency,
206
+ )
180
207
 
181
208
  #
182
209
 
@@ -207,8 +234,6 @@ class Ci(AsyncExitStacked):
207
234
  #
208
235
 
209
236
  async def run(self) -> None:
210
- await self.resolve_ci_image()
211
-
212
- await self.pull_dependencies()
237
+ await self.setup()
213
238
 
214
239
  await self._run_compose()
omdev/ci/cli.py CHANGED
@@ -88,6 +88,10 @@ class CiCli(ArgparseCli):
88
88
  argparse_arg('--github', action='store_true'),
89
89
  argparse_arg('--github-detect', action='store_true'),
90
90
 
91
+ argparse_arg('--cache-served-docker', action='store_true'),
92
+
93
+ argparse_arg('--setup-concurrency', type=int),
94
+
91
95
  argparse_arg('--always-pull', action='store_true'),
92
96
  argparse_arg('--always-build', action='store_true'),
93
97
 
@@ -203,6 +207,8 @@ class CiCli(ArgparseCli):
203
207
  always_pull=self.args.always_pull,
204
208
  always_build=self.args.always_build,
205
209
 
210
+ setup_concurrency=self.args.setup_concurrency,
211
+
206
212
  no_dependencies=self.args.no_dependencies,
207
213
 
208
214
  run_options=run_options,
@@ -225,6 +231,8 @@ class CiCli(ArgparseCli):
225
231
  directory_file_cache_config=directory_file_cache_config,
226
232
 
227
233
  github=github,
234
+
235
+ cache_served_docker=self.args.cache_served_docker,
228
236
  ))
229
237
 
230
238
  async with injector[Ci] as ci:
omdev/ci/consts.py CHANGED
@@ -1 +1 @@
1
- CI_CACHE_VERSION = 1
1
+ CI_CACHE_VERSION = 2
@@ -4,6 +4,7 @@ import dataclasses as dc
4
4
  import typing as ta
5
5
 
6
6
  from .cache import DockerCache
7
+ from .cache import DockerCacheKey
7
8
  from .cmds import is_docker_image_present
8
9
  from .cmds import tag_docker_image
9
10
 
@@ -15,7 +16,7 @@ class DockerBuildCaching(abc.ABC):
15
16
  @abc.abstractmethod
16
17
  def cached_build_docker_image(
17
18
  self,
18
- cache_key: str,
19
+ cache_key: DockerCacheKey,
19
20
  build_and_tag: ta.Callable[[str], ta.Awaitable[str]], # image_tag -> image_id
20
21
  ) -> ta.Awaitable[str]:
21
22
  raise NotImplementedError
@@ -43,10 +44,10 @@ class DockerBuildCachingImpl(DockerBuildCaching):
43
44
 
44
45
  async def cached_build_docker_image(
45
46
  self,
46
- cache_key: str,
47
+ cache_key: DockerCacheKey,
47
48
  build_and_tag: ta.Callable[[str], ta.Awaitable[str]],
48
49
  ) -> str:
49
- image_tag = f'{self._config.service}:{cache_key}'
50
+ image_tag = f'{self._config.service}:{cache_key!s}'
50
51
 
51
52
  if not self._config.always_build and (await is_docker_image_present(image_tag)):
52
53
  return image_tag
omdev/ci/docker/cache.py CHANGED
@@ -1,7 +1,9 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import abc
3
+ import dataclasses as dc
3
4
  import typing as ta
4
5
 
6
+ from omlish.lite.check import check
5
7
  from omlish.os.temp import temp_file_context
6
8
 
7
9
  from ..cache import FileCache
@@ -13,13 +15,33 @@ from .cmds import save_docker_tar_cmd
13
15
  ##
14
16
 
15
17
 
18
+ @dc.dataclass(frozen=True)
19
+ class DockerCacheKey:
20
+ prefixes: ta.Sequence[str]
21
+ content: str
22
+
23
+ def __post_init__(self) -> None:
24
+ check.not_isinstance(self.prefixes, str)
25
+
26
+ def append_prefix(self, *prefixes: str) -> 'DockerCacheKey':
27
+ return dc.replace(self, prefixes=(*self.prefixes, *prefixes))
28
+
29
+ SEPARATOR: ta.ClassVar[str] = '--'
30
+
31
+ def __str__(self) -> str:
32
+ return self.SEPARATOR.join([*self.prefixes, self.content])
33
+
34
+
35
+ ##
36
+
37
+
16
38
  class DockerCache(abc.ABC):
17
39
  @abc.abstractmethod
18
- def load_cache_docker_image(self, key: str) -> ta.Awaitable[ta.Optional[str]]:
40
+ def load_cache_docker_image(self, key: DockerCacheKey) -> ta.Awaitable[ta.Optional[str]]:
19
41
  raise NotImplementedError
20
42
 
21
43
  @abc.abstractmethod
22
- def save_cache_docker_image(self, key: str, image: str) -> ta.Awaitable[None]:
44
+ def save_cache_docker_image(self, key: DockerCacheKey, image: str) -> ta.Awaitable[None]:
23
45
  raise NotImplementedError
24
46
 
25
47
 
@@ -33,11 +55,11 @@ class DockerCacheImpl(DockerCache):
33
55
 
34
56
  self._file_cache = file_cache
35
57
 
36
- async def load_cache_docker_image(self, key: str) -> ta.Optional[str]:
58
+ async def load_cache_docker_image(self, key: DockerCacheKey) -> ta.Optional[str]:
37
59
  if self._file_cache is None:
38
60
  return None
39
61
 
40
- cache_file = await self._file_cache.get_file(key)
62
+ cache_file = await self._file_cache.get_file(str(key))
41
63
  if cache_file is None:
42
64
  return None
43
65
 
@@ -45,7 +67,7 @@ class DockerCacheImpl(DockerCache):
45
67
 
46
68
  return await load_docker_tar_cmd(get_cache_cmd)
47
69
 
48
- async def save_cache_docker_image(self, key: str, image: str) -> None:
70
+ async def save_cache_docker_image(self, key: DockerCacheKey, image: str) -> None:
49
71
  if self._file_cache is None:
50
72
  return
51
73
 
@@ -54,4 +76,4 @@ class DockerCacheImpl(DockerCache):
54
76
 
55
77
  await save_docker_tar_cmd(image, write_tmp_cmd)
56
78
 
57
- await self._file_cache.put_file(key, tmp_file, steal=True)
79
+ await self._file_cache.put_file(str(key), tmp_file, steal=True)
File without changes
@@ -0,0 +1,209 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import asyncio
3
+ import contextlib
4
+ import dataclasses as dc
5
+ import json
6
+ import os.path
7
+ import typing as ta
8
+
9
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
10
+ from omlish.lite.check import check
11
+ from omlish.lite.json import json_dumps_compact
12
+ from omlish.lite.logs import log
13
+ from omlish.lite.marshal import marshal_obj
14
+ from omlish.lite.marshal import unmarshal_obj
15
+ from omlish.lite.timeouts import Timeout
16
+ from omlish.lite.timeouts import TimeoutLike
17
+
18
+ from ....dataserver.server import DataServer
19
+ from ....dataserver.targets import DataServerTarget
20
+ from ....oci.building import build_oci_index_repository
21
+ from ....oci.data import get_single_leaf_oci_image_index
22
+ from ....oci.dataserver import build_oci_repository_data_server_routes
23
+ from ....oci.loading import read_oci_repository_root_index
24
+ from ....oci.pack.repositories import OciPackedRepositoryBuilder
25
+ from ....oci.repositories import OciRepository
26
+ from ...cache import DataCache
27
+ from ...cache import read_data_cache_data
28
+ from ..cache import DockerCache
29
+ from ..cache import DockerCacheKey
30
+ from ..dataserver import DockerDataServer
31
+ from ..repositories import DockerImageRepositoryOpener
32
+ from .manifests import CacheServedDockerImageManifest
33
+ from .manifests import build_cache_served_docker_image_data_server_routes
34
+ from .manifests import build_cache_served_docker_image_manifest
35
+
36
+
37
+ ##
38
+
39
+
40
+ class CacheServedDockerCache(DockerCache):
41
+ @dc.dataclass(frozen=True)
42
+ class Config:
43
+ port: int = 5021
44
+
45
+ repack: bool = True
46
+
47
+ key_prefix: ta.Optional[str] = 'cs'
48
+
49
+ #
50
+
51
+ pull_run_cmd: ta.Optional[str] = 'true'
52
+
53
+ #
54
+
55
+ server_start_timeout: TimeoutLike = 5.
56
+ server_start_sleep: float = .1
57
+
58
+ def __init__(
59
+ self,
60
+ *,
61
+ config: Config = Config(),
62
+
63
+ image_repo_opener: DockerImageRepositoryOpener,
64
+ data_cache: DataCache,
65
+ ) -> None:
66
+ super().__init__()
67
+
68
+ self._config = config
69
+
70
+ self._image_repo_opener = image_repo_opener
71
+ self._data_cache = data_cache
72
+
73
+ async def load_cache_docker_image(self, key: DockerCacheKey) -> ta.Optional[str]:
74
+ if (kp := self._config.key_prefix) is not None:
75
+ key = key.append_prefix(kp)
76
+
77
+ if (manifest_data := await self._data_cache.get_data(str(key))) is None:
78
+ return None
79
+
80
+ manifest_bytes = await read_data_cache_data(manifest_data)
81
+
82
+ manifest: CacheServedDockerImageManifest = unmarshal_obj(
83
+ json.loads(manifest_bytes.decode('utf-8')),
84
+ CacheServedDockerImageManifest,
85
+ )
86
+
87
+ async def make_cache_key_target(target_cache_key: str, **target_kwargs: ta.Any) -> DataServerTarget: # noqa
88
+ cache_data = check.not_none(await self._data_cache.get_data(target_cache_key))
89
+
90
+ if isinstance(cache_data, DataCache.BytesData):
91
+ return DataServerTarget.of(
92
+ cache_data.data,
93
+ **target_kwargs,
94
+ )
95
+
96
+ elif isinstance(cache_data, DataCache.FileData):
97
+ return DataServerTarget.of(
98
+ file_path=cache_data.file_path,
99
+ **target_kwargs,
100
+ )
101
+
102
+ elif isinstance(cache_data, DataCache.UrlData):
103
+ return DataServerTarget.of(
104
+ url=cache_data.url,
105
+ methods=['GET'],
106
+ **target_kwargs,
107
+ )
108
+
109
+ else:
110
+ raise TypeError(cache_data)
111
+
112
+ data_server_routes = await build_cache_served_docker_image_data_server_routes(
113
+ manifest,
114
+ make_cache_key_target,
115
+ )
116
+
117
+ data_server = DataServer(DataServer.HandlerRoute.of_(*data_server_routes))
118
+
119
+ image_url = f'localhost:{self._config.port}/{key!s}'
120
+
121
+ async with DockerDataServer(
122
+ self._config.port,
123
+ data_server,
124
+ handler_log=log,
125
+ ) as dds:
126
+ dds_run_task = asyncio.create_task(dds.run())
127
+ try:
128
+ timeout = Timeout.of(self._config.server_start_timeout)
129
+ while True:
130
+ timeout()
131
+ try:
132
+ reader, writer = await asyncio.open_connection('localhost', self._config.port)
133
+ except Exception as e: # noqa
134
+ log.exception('Failed to connect to cache server - will try again')
135
+ else:
136
+ writer.close()
137
+ await asyncio.wait_for(writer.wait_closed(), timeout=timeout.remaining())
138
+ break
139
+ await asyncio.sleep(self._config.server_start_sleep)
140
+
141
+ if (prc := self._config.pull_run_cmd) is not None:
142
+ pull_cmd = [
143
+ 'run',
144
+ '--rm',
145
+ image_url,
146
+ prc,
147
+ ]
148
+ else:
149
+ pull_cmd = [
150
+ 'pull',
151
+ image_url,
152
+ ]
153
+
154
+ await asyncio_subprocesses.check_call(
155
+ 'docker',
156
+ *pull_cmd,
157
+ )
158
+
159
+ finally:
160
+ dds.stop_event.set()
161
+ await dds_run_task
162
+
163
+ return image_url
164
+
165
+ async def save_cache_docker_image(self, key: DockerCacheKey, image: str) -> None:
166
+ if (kp := self._config.key_prefix) is not None:
167
+ key = key.append_prefix(kp)
168
+
169
+ async with contextlib.AsyncExitStack() as es:
170
+ image_repo: OciRepository = await es.enter_async_context(
171
+ self._image_repo_opener.open_docker_image_repository(image),
172
+ )
173
+
174
+ root_image_index = read_oci_repository_root_index(image_repo)
175
+ image_index = get_single_leaf_oci_image_index(root_image_index)
176
+
177
+ if self._config.repack:
178
+ prb: OciPackedRepositoryBuilder = es.enter_context(OciPackedRepositoryBuilder(
179
+ image_repo,
180
+ ))
181
+ built_repo = await asyncio.get_running_loop().run_in_executor(None, prb.build) # noqa
182
+
183
+ else:
184
+ built_repo = build_oci_index_repository(image_index)
185
+
186
+ data_server_routes = build_oci_repository_data_server_routes(
187
+ str(key),
188
+ built_repo,
189
+ )
190
+
191
+ async def make_file_cache_key(file_path: str) -> str:
192
+ target_cache_key = f'{key!s}--{os.path.basename(file_path).split(".")[0]}'
193
+ await self._data_cache.put_data(
194
+ target_cache_key,
195
+ DataCache.FileData(file_path),
196
+ )
197
+ return target_cache_key
198
+
199
+ cache_served_manifest = await build_cache_served_docker_image_manifest(
200
+ data_server_routes,
201
+ make_file_cache_key,
202
+ )
203
+
204
+ manifest_data = json_dumps_compact(marshal_obj(cache_served_manifest)).encode('utf-8')
205
+
206
+ await self._data_cache.put_data(
207
+ str(key),
208
+ DataCache.BytesData(manifest_data),
209
+ )
@@ -0,0 +1,122 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import dataclasses as dc
4
+ import os.path
5
+ import typing as ta
6
+
7
+ from omlish.lite.check import check
8
+
9
+ from ....dataserver.routes import DataServerRoute
10
+ from ....dataserver.targets import BytesDataServerTarget
11
+ from ....dataserver.targets import DataServerTarget
12
+ from ....dataserver.targets import FileDataServerTarget
13
+
14
+
15
+ ##
16
+
17
+
18
+ @dc.dataclass(frozen=True)
19
+ class CacheServedDockerImageManifest:
20
+ @dc.dataclass(frozen=True)
21
+ class Route:
22
+ paths: ta.Sequence[str]
23
+
24
+ content_type: str
25
+ content_length: int
26
+
27
+ @dc.dataclass(frozen=True)
28
+ class Target(abc.ABC): # noqa
29
+ pass
30
+
31
+ @dc.dataclass(frozen=True)
32
+ class BytesTarget(Target):
33
+ data: bytes
34
+
35
+ @dc.dataclass(frozen=True)
36
+ class CacheKeyTarget(Target):
37
+ key: str
38
+
39
+ target: Target
40
+
41
+ def __post_init__(self) -> None:
42
+ check.not_isinstance(self.paths, str)
43
+
44
+ routes: ta.Sequence[Route]
45
+
46
+
47
+ #
48
+
49
+
50
+ async def build_cache_served_docker_image_manifest(
51
+ data_server_routes: ta.Iterable[DataServerRoute],
52
+ make_file_cache_key: ta.Callable[[str], ta.Awaitable[str]],
53
+ ) -> CacheServedDockerImageManifest:
54
+ routes: ta.List[CacheServedDockerImageManifest.Route] = []
55
+
56
+ for data_server_route in data_server_routes:
57
+ content_length: int
58
+
59
+ data_server_target = data_server_route.target
60
+ target: CacheServedDockerImageManifest.Route.Target
61
+ if isinstance(data_server_target, BytesDataServerTarget):
62
+ bytes_data = check.isinstance(data_server_target.data, bytes)
63
+ content_length = len(bytes_data)
64
+ target = CacheServedDockerImageManifest.Route.BytesTarget(bytes_data)
65
+
66
+ elif isinstance(data_server_target, FileDataServerTarget):
67
+ file_path = check.non_empty_str(data_server_target.file_path)
68
+ content_length = os.path.getsize(file_path)
69
+ cache_key = await make_file_cache_key(file_path)
70
+ target = CacheServedDockerImageManifest.Route.CacheKeyTarget(cache_key)
71
+
72
+ else:
73
+ raise TypeError(data_server_target)
74
+
75
+ routes.append(CacheServedDockerImageManifest.Route(
76
+ paths=data_server_route.paths,
77
+
78
+ content_type=check.non_empty_str(data_server_target.content_type),
79
+ content_length=content_length,
80
+
81
+ target=target,
82
+ ))
83
+
84
+ return CacheServedDockerImageManifest(
85
+ routes=routes,
86
+ )
87
+
88
+
89
+ #
90
+
91
+
92
+ async def build_cache_served_docker_image_data_server_routes(
93
+ manifest: CacheServedDockerImageManifest,
94
+ make_cache_key_target: ta.Callable[..., ta.Awaitable[DataServerTarget]],
95
+ ) -> ta.List[DataServerRoute]:
96
+ routes: ta.List[DataServerRoute] = []
97
+
98
+ for manifest_route in manifest.routes:
99
+ manifest_target = manifest_route.target
100
+
101
+ target_kwargs: dict = dict(
102
+ content_type=manifest_route.content_type,
103
+ content_length=manifest_route.content_length,
104
+ )
105
+
106
+ target: DataServerTarget
107
+
108
+ if isinstance(manifest_target, CacheServedDockerImageManifest.Route.BytesTarget):
109
+ target = DataServerTarget.of(manifest_target.data, **target_kwargs)
110
+
111
+ elif isinstance(manifest_target, CacheServedDockerImageManifest.Route.CacheKeyTarget):
112
+ target = await make_cache_key_target(manifest_target.key, **target_kwargs)
113
+
114
+ else:
115
+ raise TypeError(manifest_target)
116
+
117
+ routes.append(DataServerRoute(
118
+ paths=manifest_route.paths,
119
+ target=target,
120
+ ))
121
+
122
+ return routes
@@ -7,7 +7,7 @@ import sys
7
7
  import threading
8
8
  import typing as ta
9
9
 
10
- from omlish.docker.portrelay import DockerPortRelay
10
+ from omlish.docker.ports import DockerPortRelay
11
11
  from omlish.http.coro.simple import make_simple_http_server
12
12
  from omlish.http.handlers import HttpHandler
13
13
  from omlish.http.handlers import LoggingHttpHandler
@@ -93,6 +93,7 @@ class AsyncioManagedSimpleHttpServer(AsyncExitStacked):
93
93
  self._port,
94
94
  self._handler,
95
95
  ssl_context=self._ssl_context(),
96
+ ignore_ssl_errors=True,
96
97
  use_threads=True,
97
98
  ) as server:
98
99
  yield server
@@ -163,6 +164,13 @@ class DockerDataServer(AsyncExitStacked):
163
164
  return self._stop_event
164
165
 
165
166
  async def run(self) -> None:
167
+ # FIXME:
168
+ # - shared single server with updatable routes
169
+ # - get docker used ports with ns1
170
+ # - discover server port with get_available_port
171
+ # - discover relay port pair with get_available_ports
172
+ # relay_port: ta.Optional[ta.Tuple[int, int]] = None
173
+
166
174
  relay_port: ta.Optional[int] = None
167
175
  if sys.platform == 'darwin':
168
176
  relay_port = self._port
@@ -4,9 +4,11 @@ import dataclasses as dc
4
4
  import typing as ta
5
5
 
6
6
  from omlish.lite.timing import log_timing_context
7
+ from omlish.text.mangle import StringMangler
7
8
 
8
9
  from ..cache import FileCache
9
10
  from .cache import DockerCache
11
+ from .cache import DockerCacheKey
10
12
  from .cmds import is_docker_image_present
11
13
  from .cmds import pull_docker_image
12
14
 
@@ -44,11 +46,9 @@ class DockerImagePullingImpl(DockerImagePulling):
44
46
  if not self._config.always_pull and (await is_docker_image_present(image)):
45
47
  return
46
48
 
47
- dep_suffix = image
48
- for c in '/:.-_':
49
- dep_suffix = dep_suffix.replace(c, '-')
49
+ key_content = StringMangler.of('-', '/:._').mangle(image)
50
50
 
51
- cache_key = f'docker-{dep_suffix}'
51
+ cache_key = DockerCacheKey(['docker'], key_content)
52
52
  if (
53
53
  self._docker_cache is not None and
54
54
  (await self._docker_cache.load_cache_docker_image(cache_key)) is not None