omdev 0.0.0.dev210__py3-none-any.whl → 0.0.0.dev212__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
@@ -13,19 +13,19 @@ from omlish.lite.contextmanagers import ExitStacked
13
13
  from omlish.lite.contextmanagers import defer
14
14
 
15
15
  from .cache import FileCache
16
+ from .cache import ShellCache
16
17
  from .compose import DockerComposeRun
17
18
  from .compose import get_compose_service_dependencies
18
- from .dockertars import build_docker_file_hash
19
- from .dockertars import build_docker_tar
20
- from .dockertars import is_docker_image_present
21
- from .dockertars import load_docker_tar
22
- from .dockertars import pull_docker_tar
23
- from .dockertars import read_docker_tar_image_id
19
+ from .docker import build_docker_file_hash
20
+ from .docker import build_docker_image
21
+ from .docker import is_docker_image_present
22
+ from .docker import load_docker_tar_cmd
23
+ from .docker import pull_docker_image
24
+ from .docker import save_docker_tar_cmd
24
25
  from .requirements import build_requirements_hash
25
26
  from .requirements import download_requirements
26
-
27
-
28
- ##
27
+ from .shell import ShellCmd
28
+ from .utils import log_timing_context
29
29
 
30
30
 
31
31
  class Ci(ExitStacked):
@@ -40,8 +40,12 @@ class Ci(ExitStacked):
40
40
  compose_file: str
41
41
  service: str
42
42
 
43
+ cmd: ShellCmd
44
+
43
45
  requirements_txts: ta.Optional[ta.Sequence[str]] = None
44
46
 
47
+ always_pull: bool = False
48
+
45
49
  def __post_init__(self) -> None:
46
50
  check.not_isinstance(self.requirements_txts, str)
47
51
 
@@ -49,40 +53,61 @@ class Ci(ExitStacked):
49
53
  self,
50
54
  cfg: Config,
51
55
  *,
56
+ shell_cache: ta.Optional[ShellCache] = None,
52
57
  file_cache: ta.Optional[FileCache] = None,
53
58
  ) -> None:
54
59
  super().__init__()
55
60
 
56
61
  self._cfg = cfg
62
+ self._shell_cache = shell_cache
57
63
  self._file_cache = file_cache
58
64
 
59
65
  #
60
66
 
61
- def load_docker_image(self, image: str) -> None:
62
- if is_docker_image_present(image):
67
+ def _load_cache_docker_image(self, key: str) -> ta.Optional[str]:
68
+ if self._shell_cache is None:
69
+ return None
70
+
71
+ get_cache_cmd = self._shell_cache.get_file_cmd(key)
72
+ if get_cache_cmd is None:
73
+ return None
74
+
75
+ get_cache_cmd = dc.replace(get_cache_cmd, s=f'{get_cache_cmd.s} | zstd -cd --long') # noqa
76
+
77
+ return load_docker_tar_cmd(get_cache_cmd)
78
+
79
+ def _save_cache_docker_image(self, key: str, image: str) -> None:
80
+ if self._shell_cache is None:
81
+ return
82
+
83
+ with self._shell_cache.put_file_cmd(key) as put_cache:
84
+ put_cache_cmd = put_cache.cmd
85
+
86
+ put_cache_cmd = dc.replace(put_cache_cmd, s=f'zstd | {put_cache_cmd.s}')
87
+
88
+ save_docker_tar_cmd(image, put_cache_cmd)
89
+
90
+ #
91
+
92
+ def _load_docker_image(self, image: str) -> None:
93
+ if not self._cfg.always_pull and is_docker_image_present(image):
63
94
  return
64
95
 
65
96
  dep_suffix = image
66
97
  for c in '/:.-_':
67
98
  dep_suffix = dep_suffix.replace(c, '-')
68
99
 
69
- tar_file_name = f'docker-{dep_suffix}.tar'
70
-
71
- if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_name)):
72
- load_docker_tar(cache_tar_file)
100
+ cache_key = f'docker-{dep_suffix}'
101
+ if self._load_cache_docker_image(cache_key) is not None:
73
102
  return
74
103
 
75
- temp_dir = tempfile.mkdtemp()
76
- with defer(lambda: shutil.rmtree(temp_dir)):
77
- temp_tar_file = os.path.join(temp_dir, tar_file_name)
104
+ pull_docker_image(image)
78
105
 
79
- pull_docker_tar(
80
- image,
81
- temp_tar_file,
82
- )
106
+ self._save_cache_docker_image(cache_key, image)
83
107
 
84
- if self._file_cache is not None:
85
- self._file_cache.put_file(temp_tar_file)
108
+ def load_docker_image(self, image: str) -> None:
109
+ with log_timing_context(f'Load docker image: {image}'):
110
+ self._load_docker_image(image)
86
111
 
87
112
  @cached_nullary
88
113
  def load_compose_service_dependencies(self) -> None:
@@ -96,46 +121,46 @@ class Ci(ExitStacked):
96
121
 
97
122
  #
98
123
 
99
- @cached_nullary
100
- def build_ci_image(self) -> str:
124
+ def _resolve_ci_image(self) -> str:
101
125
  docker_file_hash = build_docker_file_hash(self._cfg.docker_file)[:self.FILE_NAME_HASH_LEN]
102
126
 
103
- tar_file_name = f'ci-{docker_file_hash}.tar'
104
-
105
- if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_name)):
106
- image_id = read_docker_tar_image_id(cache_tar_file)
107
- load_docker_tar(cache_tar_file)
108
- return image_id
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
109
130
 
110
- temp_dir = tempfile.mkdtemp()
111
- with defer(lambda: shutil.rmtree(temp_dir)):
112
- temp_tar_file = os.path.join(temp_dir, tar_file_name)
131
+ image_id = build_docker_image(
132
+ self._cfg.docker_file,
133
+ cwd=self._cfg.project_dir,
134
+ )
113
135
 
114
- image_id = build_docker_tar(
115
- self._cfg.docker_file,
116
- temp_tar_file,
117
- cwd=self._cfg.project_dir,
118
- )
136
+ self._save_cache_docker_image(cache_key, image_id)
119
137
 
120
- if self._file_cache is not None:
121
- self._file_cache.put_file(temp_tar_file)
138
+ return image_id
122
139
 
140
+ @cached_nullary
141
+ def resolve_ci_image(self) -> str:
142
+ with log_timing_context('Resolve ci image') as ltc:
143
+ image_id = self._resolve_ci_image()
144
+ ltc.set_description(f'Resolve ci image: {image_id}')
123
145
  return image_id
124
146
 
125
147
  #
126
148
 
127
- @cached_nullary
128
- def build_requirements_dir(self) -> str:
129
- requirements_txts = check.not_none(self._cfg.requirements_txts)
149
+ def _resolve_requirements_dir(self) -> str:
150
+ requirements_txts = [
151
+ os.path.join(self._cfg.project_dir, rf)
152
+ for rf in check.not_none(self._cfg.requirements_txts)
153
+ ]
130
154
 
131
155
  requirements_hash = build_requirements_hash(requirements_txts)[:self.FILE_NAME_HASH_LEN]
132
156
 
133
- tar_file_name = f'requirements-{requirements_hash}.tar'
157
+ tar_file_key = f'requirements-{requirements_hash}'
158
+ tar_file_name = f'{tar_file_key}.tar'
134
159
 
135
160
  temp_dir = tempfile.mkdtemp()
136
161
  self._enter_context(defer(lambda: shutil.rmtree(temp_dir))) # noqa
137
162
 
138
- if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_name)):
163
+ if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_key)):
139
164
  with tarfile.open(cache_tar_file) as tar:
140
165
  tar.extractall(path=temp_dir) # noqa
141
166
 
@@ -145,7 +170,7 @@ class Ci(ExitStacked):
145
170
  os.makedirs(temp_requirements_dir)
146
171
 
147
172
  download_requirements(
148
- self.build_ci_image(),
173
+ self.resolve_ci_image(),
149
174
  temp_requirements_dir,
150
175
  requirements_txts,
151
176
  )
@@ -160,21 +185,20 @@ class Ci(ExitStacked):
160
185
  arcname=requirement_file,
161
186
  )
162
187
 
163
- self._file_cache.put_file(temp_tar_file)
188
+ self._file_cache.put_file(os.path.basename(tar_file_key), temp_tar_file)
164
189
 
165
190
  return temp_requirements_dir
166
191
 
167
- #
168
-
169
- def run(self) -> None:
170
- self.load_compose_service_dependencies()
171
-
172
- ci_image = self.build_ci_image()
173
-
174
- requirements_dir = self.build_requirements_dir()
192
+ @cached_nullary
193
+ def resolve_requirements_dir(self) -> str:
194
+ with log_timing_context('Resolve requirements dir') as ltc:
195
+ requirements_dir = self._resolve_requirements_dir()
196
+ ltc.set_description(f'Resolve requirements dir: {requirements_dir}')
197
+ return requirements_dir
175
198
 
176
- #
199
+ #
177
200
 
201
+ def _run_compose_(self) -> None:
178
202
  setup_cmds = [
179
203
  'pip install --root-user-action ignore --find-links /requirements --no-index uv',
180
204
  (
@@ -185,30 +209,41 @@ class Ci(ExitStacked):
185
209
 
186
210
  #
187
211
 
188
- test_cmds = [
189
- '(cd /project && python3 -m pytest -svv test.py)',
190
- ]
212
+ ci_cmd = dc.replace(self._cfg.cmd, s=' && '.join([
213
+ *setup_cmds,
214
+ f'({self._cfg.cmd.s})',
215
+ ]))
191
216
 
192
217
  #
193
218
 
194
- bash_src = ' && '.join([
195
- *setup_cmds,
196
- *test_cmds,
197
- ])
198
-
199
219
  with DockerComposeRun(DockerComposeRun.Config(
200
- compose_file=self._cfg.compose_file,
201
- service=self._cfg.service,
220
+ compose_file=self._cfg.compose_file,
221
+ service=self._cfg.service,
202
222
 
203
- image=ci_image,
223
+ image=self.resolve_ci_image(),
204
224
 
205
- run_cmd=['bash', '-c', bash_src],
225
+ cmd=ci_cmd,
206
226
 
207
- run_options=[
208
- '-v', f'{os.path.abspath(self._cfg.project_dir)}:/project',
209
- '-v', f'{os.path.abspath(requirements_dir)}:/requirements',
210
- ],
227
+ run_options=[
228
+ '-v', f'{os.path.abspath(self._cfg.project_dir)}:/project',
229
+ '-v', f'{os.path.abspath(self.resolve_requirements_dir())}:/requirements',
230
+ ],
211
231
 
212
- cwd=self._cfg.project_dir,
232
+ cwd=self._cfg.project_dir,
213
233
  )) as ci_compose_run:
214
234
  ci_compose_run.run()
235
+
236
+ def _run_compose(self) -> None:
237
+ with log_timing_context('Run compose'):
238
+ self._run_compose_()
239
+
240
+ #
241
+
242
+ def run(self) -> None:
243
+ self.load_compose_service_dependencies()
244
+
245
+ self.resolve_ci_image()
246
+
247
+ self.resolve_requirements_dir()
248
+
249
+ self._run_compose()
omdev/ci/cli.py CHANGED
@@ -20,15 +20,18 @@ from omlish.argparse.cli import ArgparseCli
20
20
  from omlish.argparse.cli import argparse_arg
21
21
  from omlish.argparse.cli import argparse_cmd
22
22
  from omlish.lite.check import check
23
+ from omlish.logs.standard import configure_standard_logging
23
24
 
24
25
  from .cache import DirectoryFileCache
26
+ from .cache import DirectoryShellCache
25
27
  from .cache import FileCache
28
+ from .cache import ShellCache
26
29
  from .ci import Ci
27
30
  from .compose import get_compose_service_dependencies
31
+ from .github.cache import GithubShellCache
32
+ from .github.cli import GithubCli
28
33
  from .requirements import build_requirements_hash
29
-
30
-
31
- ##
34
+ from .shell import ShellCmd
32
35
 
33
36
 
34
37
  class CiCli(ArgparseCli):
@@ -59,23 +62,32 @@ class CiCli(ArgparseCli):
59
62
 
60
63
  #
61
64
 
65
+ @argparse_cmd(
66
+ accepts_unknown=True,
67
+ )
68
+ def github(self) -> ta.Optional[int]:
69
+ return GithubCli(self.unknown_args).cli_run()
70
+
71
+ #
72
+
62
73
  @argparse_cmd(
63
74
  argparse_arg('project-dir'),
64
75
  argparse_arg('service'),
65
76
  argparse_arg('--docker-file'),
66
77
  argparse_arg('--compose-file'),
67
78
  argparse_arg('-r', '--requirements-txt', action='append'),
79
+ argparse_arg('--github-cache', action='store_true'),
68
80
  argparse_arg('--cache-dir'),
81
+ argparse_arg('--always-pull', action='store_true'),
69
82
  )
70
83
  async def run(self) -> None:
71
- await asyncio.sleep(1)
72
-
73
84
  project_dir = self.args.project_dir
74
85
  docker_file = self.args.docker_file
75
86
  compose_file = self.args.compose_file
76
87
  service = self.args.service
77
88
  requirements_txts = self.args.requirements_txt
78
89
  cache_dir = self.args.cache_dir
90
+ always_pull = self.args.always_pull
79
91
 
80
92
  #
81
93
 
@@ -85,7 +97,7 @@ class CiCli(ArgparseCli):
85
97
 
86
98
  def find_alt_file(*alts: str) -> ta.Optional[str]:
87
99
  for alt in alts:
88
- alt_file = os.path.join(project_dir, alt)
100
+ alt_file = os.path.abspath(os.path.join(project_dir, alt))
89
101
  if os.path.isfile(alt_file):
90
102
  return alt_file
91
103
  return None
@@ -100,10 +112,16 @@ class CiCli(ArgparseCli):
100
112
  check.state(os.path.isfile(docker_file))
101
113
 
102
114
  if compose_file is None:
103
- compose_file = find_alt_file(
104
- 'docker/compose.yml',
105
- 'compose.yml',
106
- )
115
+ compose_file = find_alt_file(*[
116
+ f'{f}.{x}'
117
+ for f in [
118
+ 'docker/docker-compose',
119
+ 'docker/compose',
120
+ 'docker-compose',
121
+ 'compose',
122
+ ]
123
+ for x in ['yaml', 'yml']
124
+ ])
107
125
  check.state(os.path.isfile(compose_file))
108
126
 
109
127
  if not requirements_txts:
@@ -121,24 +139,44 @@ class CiCli(ArgparseCli):
121
139
 
122
140
  #
123
141
 
142
+ shell_cache: ta.Optional[ShellCache] = None
124
143
  file_cache: ta.Optional[FileCache] = None
125
144
  if cache_dir is not None:
126
145
  if not os.path.exists(cache_dir):
127
146
  os.makedirs(cache_dir)
128
147
  check.state(os.path.isdir(cache_dir))
129
- file_cache = DirectoryFileCache(cache_dir)
148
+
149
+ directory_file_cache = DirectoryFileCache(cache_dir)
150
+
151
+ file_cache = directory_file_cache
152
+
153
+ if self.args.github_cache:
154
+ shell_cache = GithubShellCache(cache_dir)
155
+ else:
156
+ shell_cache = DirectoryShellCache(directory_file_cache)
130
157
 
131
158
  #
132
159
 
133
160
  with Ci(
134
161
  Ci.Config(
135
162
  project_dir=project_dir,
163
+
136
164
  docker_file=docker_file,
165
+
137
166
  compose_file=compose_file,
138
167
  service=service,
168
+
139
169
  requirements_txts=requirements_txts,
170
+
171
+ cmd=ShellCmd(' && '.join([
172
+ 'cd /project',
173
+ 'python3 -m pytest -svv test.py',
174
+ ])),
175
+
176
+ always_pull=always_pull,
140
177
  ),
141
178
  file_cache=file_cache,
179
+ shell_cache=shell_cache,
142
180
  ) as ci:
143
181
  ci.run()
144
182
 
@@ -148,6 +186,8 @@ async def _async_main() -> ta.Optional[int]:
148
186
 
149
187
 
150
188
  def _main() -> None:
189
+ configure_standard_logging('DEBUG')
190
+
151
191
  sys.exit(rc if isinstance(rc := asyncio.run(_async_main()), int) else 0)
152
192
 
153
193
 
omdev/ci/compose.py CHANGED
@@ -4,8 +4,11 @@
4
4
  TODO:
5
5
  - fix rmi - only when not referenced anymore
6
6
  """
7
+ import contextlib
7
8
  import dataclasses as dc
9
+ import itertools
8
10
  import os.path
11
+ import shlex
9
12
  import typing as ta
10
13
 
11
14
  from omlish.lite.cached import cached_nullary
@@ -15,6 +18,7 @@ from omlish.lite.contextmanagers import defer
15
18
  from omlish.lite.json import json_dumps_pretty
16
19
  from omlish.subprocesses import subprocesses
17
20
 
21
+ from .shell import ShellCmd
18
22
  from .utils import make_temp_file
19
23
  from .utils import read_yaml_file
20
24
 
@@ -50,7 +54,7 @@ class DockerComposeRun(ExitStacked):
50
54
 
51
55
  image: str
52
56
 
53
- run_cmd: ta.Sequence[str]
57
+ cmd: ShellCmd
54
58
 
55
59
  #
56
60
 
@@ -60,9 +64,11 @@ class DockerComposeRun(ExitStacked):
60
64
 
61
65
  #
62
66
 
63
- def __post_init__(self) -> None:
64
- check.not_isinstance(self.run_cmd, str)
67
+ no_dependency_cleanup: bool = False
68
+
69
+ #
65
70
 
71
+ def __post_init__(self) -> None:
66
72
  check.not_isinstance(self.run_options, str)
67
73
 
68
74
  def __init__(self, cfg: Config) -> None:
@@ -171,28 +177,41 @@ class DockerComposeRun(ExitStacked):
171
177
 
172
178
  #
173
179
 
180
+ def _cleanup_dependencies(self) -> None:
181
+ subprocesses.check_call(
182
+ 'docker',
183
+ 'compose',
184
+ '-f', self.rewrite_compose_file(),
185
+ 'down',
186
+ )
187
+
174
188
  def run(self) -> None:
175
189
  self.tag_image()
176
190
 
177
191
  compose_file = self.rewrite_compose_file()
178
192
 
179
- try:
180
- subprocesses.check_call(
193
+ with contextlib.ExitStack() as es:
194
+ if not self._cfg.no_dependency_cleanup:
195
+ es.enter_context(defer(self._cleanup_dependencies)) # noqa
196
+
197
+ sh_cmd = ' '.join([
181
198
  'docker',
182
199
  'compose',
183
200
  '-f', compose_file,
184
201
  'run',
185
202
  '--rm',
186
- *self._cfg.run_options or [],
203
+ *itertools.chain.from_iterable(
204
+ ['-e', k]
205
+ for k in (self._cfg.cmd.env or [])
206
+ ),
207
+ *(self._cfg.run_options or []),
187
208
  self._cfg.service,
188
- *self._cfg.run_cmd,
189
- **self._subprocess_kwargs,
190
- )
209
+ 'sh', '-c', shlex.quote(self._cfg.cmd.s),
210
+ ])
191
211
 
192
- finally:
193
- subprocesses.check_call(
194
- 'docker',
195
- 'compose',
196
- '-f', compose_file,
197
- 'down',
212
+ run_cmd = dc.replace(self._cfg.cmd, s=sh_cmd)
213
+
214
+ run_cmd.run(
215
+ subprocesses.check_call,
216
+ **self._subprocess_kwargs,
198
217
  )
@@ -6,8 +6,10 @@ TODO:
6
6
  - doesn't change too much though
7
7
  """
8
8
  import contextlib
9
+ import dataclasses as dc
9
10
  import json
10
11
  import os.path
12
+ import shlex
11
13
  import tarfile
12
14
  import typing as ta
13
15
 
@@ -15,6 +17,7 @@ from omlish.lite.check import check
15
17
  from omlish.lite.contextmanagers import defer
16
18
  from omlish.subprocesses import subprocesses
17
19
 
20
+ from .shell import ShellCmd
18
21
  from .utils import make_temp_file
19
22
  from .utils import sha256_str
20
23
 
@@ -73,12 +76,8 @@ def is_docker_image_present(image: str) -> bool:
73
76
  return True
74
77
 
75
78
 
76
- ##
77
-
78
-
79
- def pull_docker_tar(
79
+ def pull_docker_image(
80
80
  image: str,
81
- tar_file: str,
82
81
  ) -> None:
83
82
  subprocesses.check_call(
84
83
  'docker',
@@ -86,19 +85,11 @@ def pull_docker_tar(
86
85
  image,
87
86
  )
88
87
 
89
- subprocesses.check_call(
90
- 'docker',
91
- 'save',
92
- image,
93
- '-o', tar_file,
94
- )
95
-
96
88
 
97
- def build_docker_tar(
98
- docker_file: str,
99
- tar_file: str,
100
- *,
101
- cwd: ta.Optional[str] = None,
89
+ def build_docker_image(
90
+ docker_file: str,
91
+ *,
92
+ cwd: ta.Optional[str] = None,
102
93
  ) -> str:
103
94
  id_file = make_temp_file()
104
95
  with defer(lambda: os.unlink(id_file)):
@@ -115,24 +106,46 @@ def build_docker_tar(
115
106
  with open(id_file) as f:
116
107
  image_id = check.single(f.read().strip().splitlines()).strip()
117
108
 
118
- subprocesses.check_call(
119
- 'docker',
120
- 'save',
121
- image_id,
122
- '-o', tar_file,
123
- )
124
-
125
- return image_id
109
+ return image_id
126
110
 
127
111
 
128
112
  ##
129
113
 
130
114
 
131
- def load_docker_tar(
115
+ def save_docker_tar_cmd(
116
+ image: str,
117
+ output_cmd: ShellCmd,
118
+ ) -> None:
119
+ cmd = dc.replace(output_cmd, s=f'docker save {image} | {output_cmd.s}')
120
+ cmd.run(subprocesses.check_call)
121
+
122
+
123
+ def save_docker_tar(
124
+ image: str,
132
125
  tar_file: str,
133
126
  ) -> None:
134
- subprocesses.check_call(
135
- 'docker',
136
- 'load',
137
- '-i', tar_file,
127
+ return save_docker_tar_cmd(
128
+ image,
129
+ ShellCmd(f'cat > {shlex.quote(tar_file)}'),
138
130
  )
131
+
132
+
133
+ #
134
+
135
+
136
+ def load_docker_tar_cmd(
137
+ input_cmd: ShellCmd,
138
+ ) -> str:
139
+ cmd = dc.replace(input_cmd, s=f'{input_cmd.s} | docker load')
140
+
141
+ out = cmd.run(subprocesses.check_output).decode()
142
+
143
+ line = check.single(out.strip().splitlines())
144
+ loaded = line.partition(':')[2].strip()
145
+ return loaded
146
+
147
+
148
+ def load_docker_tar(
149
+ tar_file: str,
150
+ ) -> str:
151
+ return load_docker_tar_cmd(ShellCmd(f'cat {shlex.quote(tar_file)}'))
File without changes
@@ -0,0 +1,11 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ sudo rm -rf \
5
+ /usr/local/.ghcup \
6
+ /opt/hostedtoolcache \
7
+
8
+ /usr/local/.ghcup 6.4G, 3391250 files
9
+ /opt/hostedtoolcache 8.0G, 14843980 files
10
+ /usr/local/lib/android 6.4G, 17251667 files
11
+ """