omdev 0.0.0.dev211__py3-none-any.whl → 0.0.0.dev213__py3-none-any.whl

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