omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__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.
Files changed (48) hide show
  1. omdev/ci/cache.py +148 -23
  2. omdev/ci/ci.py +50 -110
  3. omdev/ci/cli.py +24 -23
  4. omdev/ci/docker/__init__.py +0 -0
  5. omdev/ci/docker/buildcaching.py +69 -0
  6. omdev/ci/docker/cache.py +57 -0
  7. omdev/ci/docker/cacheserved.py +262 -0
  8. omdev/ci/{docker.py → docker/cmds.py} +1 -44
  9. omdev/ci/docker/dataserver.py +204 -0
  10. omdev/ci/docker/imagepulling.py +65 -0
  11. omdev/ci/docker/inject.py +37 -0
  12. omdev/ci/docker/packing.py +72 -0
  13. omdev/ci/docker/repositories.py +40 -0
  14. omdev/ci/docker/utils.py +48 -0
  15. omdev/ci/github/cache.py +35 -6
  16. omdev/ci/github/client.py +9 -2
  17. omdev/ci/github/inject.py +30 -0
  18. omdev/ci/inject.py +61 -0
  19. omdev/ci/utils.py +0 -49
  20. omdev/dataserver/__init__.py +1 -0
  21. omdev/dataserver/handlers.py +198 -0
  22. omdev/dataserver/http.py +69 -0
  23. omdev/dataserver/routes.py +49 -0
  24. omdev/dataserver/server.py +90 -0
  25. omdev/dataserver/targets.py +121 -0
  26. omdev/oci/building.py +107 -9
  27. omdev/oci/compression.py +8 -0
  28. omdev/oci/data.py +43 -0
  29. omdev/oci/datarefs.py +90 -50
  30. omdev/oci/dataserver.py +64 -0
  31. omdev/oci/loading.py +20 -0
  32. omdev/oci/media.py +20 -0
  33. omdev/oci/pack/__init__.py +0 -0
  34. omdev/oci/pack/packing.py +185 -0
  35. omdev/oci/pack/repositories.py +162 -0
  36. omdev/oci/pack/unpacking.py +204 -0
  37. omdev/oci/repositories.py +84 -2
  38. omdev/oci/tars.py +144 -0
  39. omdev/pyproject/resources/python.sh +1 -1
  40. omdev/scripts/ci.py +2137 -512
  41. omdev/scripts/interp.py +119 -22
  42. omdev/scripts/pyproject.py +141 -28
  43. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/METADATA +2 -2
  44. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/RECORD +48 -23
  45. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/LICENSE +0 -0
  46. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/WHEEL +0 -0
  47. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
  48. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/top_level.txt +0 -0
omdev/ci/cache.py CHANGED
@@ -1,34 +1,45 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import abc
3
+ import asyncio
4
+ import dataclasses as dc
5
+ import functools
3
6
  import os.path
4
7
  import shutil
5
8
  import typing as ta
9
+ import urllib.request
6
10
 
7
11
  from omlish.lite.cached import cached_nullary
8
12
  from omlish.lite.check import check
9
13
  from omlish.lite.logs import log
14
+ from omlish.os.temp import make_temp_file
10
15
 
11
16
  from .consts import CI_CACHE_VERSION
12
17
 
13
18
 
19
+ CacheVersion = ta.NewType('CacheVersion', int)
20
+
21
+
14
22
  ##
15
23
 
16
24
 
17
- @abc.abstractmethod
18
25
  class FileCache(abc.ABC):
26
+ DEFAULT_CACHE_VERSION: ta.ClassVar[CacheVersion] = CacheVersion(CI_CACHE_VERSION)
27
+
19
28
  def __init__(
20
29
  self,
21
30
  *,
22
- version: int = CI_CACHE_VERSION,
31
+ version: ta.Optional[CacheVersion] = None,
23
32
  ) -> None:
24
33
  super().__init__()
25
34
 
35
+ if version is None:
36
+ version = self.DEFAULT_CACHE_VERSION
26
37
  check.isinstance(version, int)
27
38
  check.arg(version >= 0)
28
- self._version = version
39
+ self._version: CacheVersion = version
29
40
 
30
41
  @property
31
- def version(self) -> int:
42
+ def version(self) -> CacheVersion:
32
43
  return self._version
33
44
 
34
45
  #
@@ -52,19 +63,28 @@ class FileCache(abc.ABC):
52
63
 
53
64
 
54
65
  class DirectoryFileCache(FileCache):
66
+ @dc.dataclass(frozen=True)
67
+ class Config:
68
+ dir: str
69
+
70
+ no_create: bool = False
71
+ no_purge: bool = False
72
+
55
73
  def __init__(
56
74
  self,
57
- dir: str, # noqa
75
+ config: Config,
58
76
  *,
59
- no_create: bool = False,
60
- no_purge: bool = False,
61
- **kwargs: ta.Any,
77
+ version: ta.Optional[CacheVersion] = None,
62
78
  ) -> None: # noqa
63
- super().__init__(**kwargs)
79
+ super().__init__(
80
+ version=version,
81
+ )
82
+
83
+ self._config = config
64
84
 
65
- self._dir = dir
66
- self._no_create = no_create
67
- self._no_purge = no_purge
85
+ @property
86
+ def dir(self) -> str:
87
+ return self._config.dir
68
88
 
69
89
  #
70
90
 
@@ -72,37 +92,38 @@ class DirectoryFileCache(FileCache):
72
92
 
73
93
  @cached_nullary
74
94
  def setup_dir(self) -> None:
75
- version_file = os.path.join(self._dir, self.VERSION_FILE_NAME)
95
+ version_file = os.path.join(self.dir, self.VERSION_FILE_NAME)
76
96
 
77
- if self._no_create:
78
- check.state(os.path.isdir(self._dir))
97
+ if self._config.no_create:
98
+ check.state(os.path.isdir(self.dir))
79
99
 
80
- elif not os.path.isdir(self._dir):
81
- os.makedirs(self._dir)
100
+ elif not os.path.isdir(self.dir):
101
+ os.makedirs(self.dir)
82
102
  with open(version_file, 'w') as f:
83
103
  f.write(str(self._version))
84
104
  return
85
105
 
106
+ # NOTE: intentionally raises FileNotFoundError to refuse to use an existing non-cache dir as a cache dir.
86
107
  with open(version_file) as f:
87
108
  dir_version = int(f.read().strip())
88
109
 
89
110
  if dir_version == self._version:
90
111
  return
91
112
 
92
- if self._no_purge:
113
+ if self._config.no_purge:
93
114
  raise RuntimeError(f'{dir_version=} != {self._version=}')
94
115
 
95
- dirs = [n for n in sorted(os.listdir(self._dir)) if os.path.isdir(os.path.join(self._dir, n))]
116
+ dirs = [n for n in sorted(os.listdir(self.dir)) if os.path.isdir(os.path.join(self.dir, n))]
96
117
  if dirs:
97
118
  raise RuntimeError(
98
- f'Refusing to remove stale cache dir {self._dir!r} '
119
+ f'Refusing to remove stale cache dir {self.dir!r} '
99
120
  f'due to present directories: {", ".join(dirs)}',
100
121
  )
101
122
 
102
- for n in sorted(os.listdir(self._dir)):
123
+ for n in sorted(os.listdir(self.dir)):
103
124
  if n.startswith('.'):
104
125
  continue
105
- fp = os.path.join(self._dir, n)
126
+ fp = os.path.join(self.dir, n)
106
127
  check.state(os.path.isfile(fp))
107
128
  log.debug('Purging stale cache file: %s', fp)
108
129
  os.unlink(fp)
@@ -119,7 +140,7 @@ class DirectoryFileCache(FileCache):
119
140
  key: str,
120
141
  ) -> str:
121
142
  self.setup_dir()
122
- return os.path.join(self._dir, key)
143
+ return os.path.join(self.dir, key)
123
144
 
124
145
  def format_incomplete_file(self, f: str) -> str:
125
146
  return os.path.join(os.path.dirname(f), f'_{os.path.basename(f)}.incomplete')
@@ -145,3 +166,107 @@ class DirectoryFileCache(FileCache):
145
166
  else:
146
167
  shutil.copyfile(file_path, cache_file_path)
147
168
  return cache_file_path
169
+
170
+
171
+ ##
172
+
173
+
174
+ class DataCache:
175
+ @dc.dataclass(frozen=True)
176
+ class Data(abc.ABC): # noqa
177
+ pass
178
+
179
+ @dc.dataclass(frozen=True)
180
+ class BytesData(Data):
181
+ data: bytes
182
+
183
+ @dc.dataclass(frozen=True)
184
+ class FileData(Data):
185
+ file_path: str
186
+
187
+ @dc.dataclass(frozen=True)
188
+ class UrlData(Data):
189
+ url: str
190
+
191
+ #
192
+
193
+ @abc.abstractmethod
194
+ def get_data(self, key: str) -> ta.Awaitable[ta.Optional[Data]]:
195
+ raise NotImplementedError
196
+
197
+ @abc.abstractmethod
198
+ def put_data(self, key: str, data: Data) -> ta.Awaitable[None]:
199
+ raise NotImplementedError
200
+
201
+
202
+ #
203
+
204
+
205
+ @functools.singledispatch
206
+ async def read_data_cache_data(data: DataCache.Data) -> bytes:
207
+ raise TypeError(data)
208
+
209
+
210
+ @read_data_cache_data.register
211
+ async def _(data: DataCache.BytesData) -> bytes:
212
+ return data.data
213
+
214
+
215
+ @read_data_cache_data.register
216
+ async def _(data: DataCache.FileData) -> bytes:
217
+ with open(data.file_path, 'rb') as f: # noqa
218
+ return f.read()
219
+
220
+
221
+ @read_data_cache_data.register
222
+ async def _(data: DataCache.UrlData) -> bytes:
223
+ def inner() -> bytes:
224
+ with urllib.request.urlopen(urllib.request.Request( # noqa
225
+ data.url,
226
+ )) as resp:
227
+ return resp.read()
228
+
229
+ return await asyncio.get_running_loop().run_in_executor(None, inner)
230
+
231
+
232
+ #
233
+
234
+
235
+ class FileCacheDataCache(DataCache):
236
+ def __init__(
237
+ self,
238
+ file_cache: FileCache,
239
+ ) -> None:
240
+ super().__init__()
241
+
242
+ self._file_cache = file_cache
243
+
244
+ async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
245
+ if (file_path := await self._file_cache.get_file(key)) is None:
246
+ return None
247
+
248
+ return DataCache.FileData(file_path)
249
+
250
+ async def put_data(self, key: str, data: DataCache.Data) -> None:
251
+ steal = False
252
+
253
+ if isinstance(data, DataCache.BytesData):
254
+ file_path = make_temp_file()
255
+ with open(file_path, 'wb') as f: # noqa
256
+ f.write(data.data)
257
+ steal = True
258
+
259
+ elif isinstance(data, DataCache.FileData):
260
+ file_path = data.file_path
261
+
262
+ elif isinstance(data, DataCache.UrlData):
263
+ raise NotImplementedError
264
+
265
+ else:
266
+ raise TypeError(data)
267
+
268
+ await self._file_cache.put_file(
269
+ key,
270
+ file_path,
271
+ steal=steal,
272
+ )
omdev/ci/ci.py CHANGED
@@ -7,21 +7,20 @@ from omlish.lite.cached import async_cached_nullary
7
7
  from omlish.lite.cached import cached_nullary
8
8
  from omlish.lite.check import check
9
9
  from omlish.lite.contextmanagers import AsyncExitStacked
10
+ from omlish.lite.timing import log_timing_context
10
11
  from omlish.os.temp import temp_file_context
11
12
 
12
- from .cache import FileCache
13
13
  from .compose import DockerComposeRun
14
14
  from .compose import get_compose_service_dependencies
15
- from .docker import build_docker_file_hash
16
- from .docker import build_docker_image
17
- from .docker import is_docker_image_present
18
- from .docker import load_docker_tar_cmd
19
- from .docker import pull_docker_image
20
- from .docker import save_docker_tar_cmd
21
- from .docker import tag_docker_image
15
+ from .docker.buildcaching import DockerBuildCaching
16
+ from .docker.cmds import build_docker_image
17
+ from .docker.imagepulling import DockerImagePulling
18
+ from .docker.utils import build_docker_file_hash
22
19
  from .requirements import build_requirements_hash
23
20
  from .shell import ShellCmd
24
- from .utils import log_timing_context
21
+
22
+
23
+ ##
25
24
 
26
25
 
27
26
  class Ci(AsyncExitStacked):
@@ -56,104 +55,40 @@ class Ci(AsyncExitStacked):
56
55
 
57
56
  def __init__(
58
57
  self,
59
- cfg: Config,
58
+ config: Config,
60
59
  *,
61
- file_cache: ta.Optional[FileCache] = None,
60
+ docker_build_caching: DockerBuildCaching,
61
+ docker_image_pulling: DockerImagePulling,
62
62
  ) -> None:
63
63
  super().__init__()
64
64
 
65
- self._cfg = cfg
66
- self._file_cache = file_cache
67
-
68
- #
69
-
70
- async def _load_docker_image(self, image: str) -> None:
71
- if not self._cfg.always_pull and (await is_docker_image_present(image)):
72
- return
73
-
74
- dep_suffix = image
75
- for c in '/:.-_':
76
- dep_suffix = dep_suffix.replace(c, '-')
77
-
78
- cache_key = f'docker-{dep_suffix}'
79
- if (await self._load_cache_docker_image(cache_key)) is not None:
80
- return
81
-
82
- await pull_docker_image(image)
83
-
84
- await self._save_cache_docker_image(cache_key, image)
65
+ self._config = config
85
66
 
86
- async def load_docker_image(self, image: str) -> None:
87
- with log_timing_context(f'Load docker image: {image}'):
88
- await self._load_docker_image(image)
89
-
90
- #
91
-
92
- async def _load_cache_docker_image(self, key: str) -> ta.Optional[str]:
93
- if self._file_cache is None:
94
- return None
95
-
96
- cache_file = await self._file_cache.get_file(key)
97
- if cache_file is None:
98
- return None
99
-
100
- get_cache_cmd = ShellCmd(f'cat {cache_file} | zstd -cd --long')
101
-
102
- return await load_docker_tar_cmd(get_cache_cmd)
103
-
104
- async def _save_cache_docker_image(self, key: str, image: str) -> None:
105
- if self._file_cache is None:
106
- return
107
-
108
- with temp_file_context() as tmp_file:
109
- write_tmp_cmd = ShellCmd(f'zstd > {tmp_file}')
110
-
111
- await save_docker_tar_cmd(image, write_tmp_cmd)
112
-
113
- await self._file_cache.put_file(key, tmp_file, steal=True)
114
-
115
- #
116
-
117
- async def _resolve_docker_image(
118
- self,
119
- cache_key: str,
120
- build_and_tag: ta.Callable[[str], ta.Awaitable[str]],
121
- ) -> str:
122
- image_tag = f'{self._cfg.service}:{cache_key}'
123
-
124
- if not self._cfg.always_build and (await is_docker_image_present(image_tag)):
125
- return image_tag
126
-
127
- if (cache_image_id := await self._load_cache_docker_image(cache_key)) is not None:
128
- await tag_docker_image(
129
- cache_image_id,
130
- image_tag,
131
- )
132
- return image_tag
133
-
134
- image_id = await build_and_tag(image_tag)
135
-
136
- await self._save_cache_docker_image(cache_key, image_id)
137
-
138
- return image_tag
67
+ self._docker_build_caching = docker_build_caching
68
+ self._docker_image_pulling = docker_image_pulling
139
69
 
140
70
  #
141
71
 
142
72
  @cached_nullary
143
73
  def docker_file_hash(self) -> str:
144
- return build_docker_file_hash(self._cfg.docker_file)[:self.KEY_HASH_LEN]
74
+ return build_docker_file_hash(self._config.docker_file)[:self.KEY_HASH_LEN]
75
+
76
+ @cached_nullary
77
+ def ci_base_image_cache_key(self) -> str:
78
+ return f'ci-base-{self.docker_file_hash()}'
145
79
 
146
80
  async def _resolve_ci_base_image(self) -> str:
147
81
  async def build_and_tag(image_tag: str) -> str:
148
82
  return await build_docker_image(
149
- self._cfg.docker_file,
83
+ self._config.docker_file,
150
84
  tag=image_tag,
151
- cwd=self._cfg.project_dir,
85
+ cwd=self._config.project_dir,
152
86
  )
153
87
 
154
- cache_key = f'ci-base-{self.docker_file_hash()}'
155
-
156
- return await self._resolve_docker_image(cache_key, build_and_tag)
88
+ return await self._docker_build_caching.cached_build_docker_image(
89
+ self.ci_base_image_cache_key(),
90
+ build_and_tag,
91
+ )
157
92
 
158
93
  @async_cached_nullary
159
94
  async def resolve_ci_base_image(self) -> str:
@@ -167,14 +102,18 @@ class Ci(AsyncExitStacked):
167
102
  @cached_nullary
168
103
  def requirements_txts(self) -> ta.Sequence[str]:
169
104
  return [
170
- os.path.join(self._cfg.project_dir, rf)
171
- for rf in check.not_none(self._cfg.requirements_txts)
105
+ os.path.join(self._config.project_dir, rf)
106
+ for rf in check.not_none(self._config.requirements_txts)
172
107
  ]
173
108
 
174
109
  @cached_nullary
175
110
  def requirements_hash(self) -> str:
176
111
  return build_requirements_hash(self.requirements_txts())[:self.KEY_HASH_LEN]
177
112
 
113
+ @cached_nullary
114
+ def ci_image_cache_key(self) -> str:
115
+ return f'ci-{self.docker_file_hash()}-{self.requirements_hash()}'
116
+
178
117
  async def _resolve_ci_image(self) -> str:
179
118
  async def build_and_tag(image_tag: str) -> str:
180
119
  base_image = await self.resolve_ci_base_image()
@@ -191,7 +130,7 @@ class Ci(AsyncExitStacked):
191
130
  '--no-cache',
192
131
  '--index-strategy unsafe-best-match',
193
132
  '--system',
194
- *[f'-r /project/{rf}' for rf in self._cfg.requirements_txts or []],
133
+ *[f'-r /project/{rf}' for rf in self._config.requirements_txts or []],
195
134
  ]),
196
135
  ]
197
136
  setup_cmd = ' && '.join(setup_cmds)
@@ -199,7 +138,7 @@ class Ci(AsyncExitStacked):
199
138
  docker_file_lines = [
200
139
  f'FROM {base_image}',
201
140
  'RUN mkdir /project',
202
- *[f'COPY {rf} /project/{rf}' for rf in self._cfg.requirements_txts or []],
141
+ *[f'COPY {rf} /project/{rf}' for rf in self._config.requirements_txts or []],
203
142
  f'RUN {setup_cmd}',
204
143
  'RUN rm /project/*',
205
144
  'WORKDIR /project',
@@ -212,12 +151,13 @@ class Ci(AsyncExitStacked):
212
151
  return await build_docker_image(
213
152
  docker_file,
214
153
  tag=image_tag,
215
- cwd=self._cfg.project_dir,
154
+ cwd=self._config.project_dir,
216
155
  )
217
156
 
218
- cache_key = f'ci-{self.docker_file_hash()}-{self.requirements_hash()}'
219
-
220
- return await self._resolve_docker_image(cache_key, build_and_tag)
157
+ return await self._docker_build_caching.cached_build_docker_image(
158
+ self.ci_image_cache_key(),
159
+ build_and_tag,
160
+ )
221
161
 
222
162
  @async_cached_nullary
223
163
  async def resolve_ci_image(self) -> str:
@@ -229,34 +169,34 @@ class Ci(AsyncExitStacked):
229
169
  #
230
170
 
231
171
  @async_cached_nullary
232
- async def load_dependencies(self) -> None:
172
+ async def pull_dependencies(self) -> None:
233
173
  deps = get_compose_service_dependencies(
234
- self._cfg.compose_file,
235
- self._cfg.service,
174
+ self._config.compose_file,
175
+ self._config.service,
236
176
  )
237
177
 
238
178
  for dep_image in deps.values():
239
- await self.load_docker_image(dep_image)
179
+ await self._docker_image_pulling.pull_docker_image(dep_image)
240
180
 
241
181
  #
242
182
 
243
183
  async def _run_compose_(self) -> None:
244
184
  async with DockerComposeRun(DockerComposeRun.Config(
245
- compose_file=self._cfg.compose_file,
246
- service=self._cfg.service,
185
+ compose_file=self._config.compose_file,
186
+ service=self._config.service,
247
187
 
248
188
  image=await self.resolve_ci_image(),
249
189
 
250
- cmd=self._cfg.cmd,
190
+ cmd=self._config.cmd,
251
191
 
252
192
  run_options=[
253
- '-v', f'{os.path.abspath(self._cfg.project_dir)}:/project',
254
- *(self._cfg.run_options or []),
193
+ '-v', f'{os.path.abspath(self._config.project_dir)}:/project',
194
+ *(self._config.run_options or []),
255
195
  ],
256
196
 
257
- cwd=self._cfg.project_dir,
197
+ cwd=self._config.project_dir,
258
198
 
259
- no_dependencies=self._cfg.no_dependencies,
199
+ no_dependencies=self._config.no_dependencies,
260
200
  )) as ci_compose_run:
261
201
  await ci_compose_run.run()
262
202
 
@@ -269,6 +209,6 @@ class Ci(AsyncExitStacked):
269
209
  async def run(self) -> None:
270
210
  await self.resolve_ci_image()
271
211
 
272
- await self.load_dependencies()
212
+ await self.pull_dependencies()
273
213
 
274
214
  await self._run_compose()
omdev/ci/cli.py CHANGED
@@ -21,16 +21,15 @@ from omlish.argparse.cli import ArgparseCli
21
21
  from omlish.argparse.cli import argparse_arg
22
22
  from omlish.argparse.cli import argparse_cmd
23
23
  from omlish.lite.check import check
24
+ from omlish.lite.inject import inj
24
25
  from omlish.lite.logs import log
25
26
  from omlish.logs.standard import configure_standard_logging
26
27
 
27
- from .cache import DirectoryFileCache
28
- from .cache import FileCache
29
28
  from .ci import Ci
30
29
  from .compose import get_compose_service_dependencies
31
30
  from .github.bootstrap import is_in_github_actions
32
- from .github.cache import GithubFileCache
33
31
  from .github.cli import GithubCli
32
+ from .inject import bind_ci
34
33
  from .requirements import build_requirements_hash
35
34
  from .shell import ShellCmd
36
35
 
@@ -165,14 +164,9 @@ class CiCli(ArgparseCli):
165
164
 
166
165
  #
167
166
 
168
- file_cache: ta.Optional[FileCache] = None
169
167
  if cache_dir is not None:
170
168
  cache_dir = os.path.abspath(cache_dir)
171
169
  log.debug('Using cache dir %s', cache_dir)
172
- if github:
173
- file_cache = GithubFileCache(cache_dir)
174
- else:
175
- file_cache = DirectoryFileCache(cache_dir)
176
170
 
177
171
  #
178
172
 
@@ -188,28 +182,35 @@ class CiCli(ArgparseCli):
188
182
 
189
183
  #
190
184
 
191
- async with Ci(
192
- Ci.Config(
193
- project_dir=project_dir,
185
+ config = Ci.Config(
186
+ project_dir=project_dir,
187
+
188
+ docker_file=docker_file,
189
+
190
+ compose_file=compose_file,
191
+ service=self.args.service,
194
192
 
195
- docker_file=docker_file,
193
+ requirements_txts=requirements_txts,
196
194
 
197
- compose_file=compose_file,
198
- service=self.args.service,
195
+ cmd=ShellCmd(cmd),
199
196
 
200
- requirements_txts=requirements_txts,
197
+ always_pull=self.args.always_pull,
198
+ always_build=self.args.always_build,
201
199
 
202
- cmd=ShellCmd(cmd),
200
+ no_dependencies=self.args.no_dependencies,
203
201
 
204
- always_pull=self.args.always_pull,
205
- always_build=self.args.always_build,
202
+ run_options=run_options,
203
+ )
206
204
 
207
- no_dependencies=self.args.no_dependencies,
205
+ injector = inj.create_injector(bind_ci(
206
+ config=config,
207
+
208
+ github=github,
209
+
210
+ cache_dir=cache_dir,
211
+ ))
208
212
 
209
- run_options=run_options,
210
- ),
211
- file_cache=file_cache,
212
- ) as ci:
213
+ async with injector[Ci] as ci:
213
214
  await ci.run()
214
215
 
215
216
 
File without changes
@@ -0,0 +1,69 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import dataclasses as dc
4
+ import typing as ta
5
+
6
+ from .cache import DockerCache
7
+ from .cmds import is_docker_image_present
8
+ from .cmds import tag_docker_image
9
+
10
+
11
+ ##
12
+
13
+
14
+ class DockerBuildCaching(abc.ABC):
15
+ @abc.abstractmethod
16
+ def cached_build_docker_image(
17
+ self,
18
+ cache_key: str,
19
+ build_and_tag: ta.Callable[[str], ta.Awaitable[str]], # image_tag -> image_id
20
+ ) -> ta.Awaitable[str]:
21
+ raise NotImplementedError
22
+
23
+
24
+ class DockerBuildCachingImpl(DockerBuildCaching):
25
+ @dc.dataclass(frozen=True)
26
+ class Config:
27
+ service: str
28
+
29
+ always_build: bool = False
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ config: Config,
35
+
36
+ docker_cache: ta.Optional[DockerCache] = None,
37
+ ) -> None:
38
+ super().__init__()
39
+
40
+ self._config = config
41
+
42
+ self._docker_cache = docker_cache
43
+
44
+ async def cached_build_docker_image(
45
+ self,
46
+ cache_key: str,
47
+ build_and_tag: ta.Callable[[str], ta.Awaitable[str]],
48
+ ) -> str:
49
+ image_tag = f'{self._config.service}:{cache_key}'
50
+
51
+ if not self._config.always_build and (await is_docker_image_present(image_tag)):
52
+ return image_tag
53
+
54
+ if (
55
+ self._docker_cache is not None and
56
+ (cache_image_id := await self._docker_cache.load_cache_docker_image(cache_key)) is not None
57
+ ):
58
+ await tag_docker_image(
59
+ cache_image_id,
60
+ image_tag,
61
+ )
62
+ return image_tag
63
+
64
+ image_id = await build_and_tag(image_tag)
65
+
66
+ if self._docker_cache is not None:
67
+ await self._docker_cache.save_cache_docker_image(cache_key, image_id)
68
+
69
+ return image_tag