omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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