omdev 0.0.0.dev209__py3-none-any.whl → 0.0.0.dev210__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
omdev/ci/compose.py ADDED
@@ -0,0 +1,198 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - fix rmi - only when not referenced anymore
6
+ """
7
+ import dataclasses as dc
8
+ import os.path
9
+ import typing as ta
10
+
11
+ from omlish.lite.cached import cached_nullary
12
+ from omlish.lite.check import check
13
+ from omlish.lite.contextmanagers import ExitStacked
14
+ from omlish.lite.contextmanagers import defer
15
+ from omlish.lite.json import json_dumps_pretty
16
+ from omlish.subprocesses import subprocesses
17
+
18
+ from .utils import make_temp_file
19
+ from .utils import read_yaml_file
20
+
21
+
22
+ ##
23
+
24
+
25
+ def get_compose_service_dependencies(
26
+ compose_file: str,
27
+ service: str,
28
+ ) -> ta.Dict[str, str]:
29
+ compose_dct = read_yaml_file(compose_file)
30
+
31
+ services = compose_dct['services']
32
+ service_dct = services[service]
33
+
34
+ out = {}
35
+ for dep_service in service_dct.get('depends_on', []):
36
+ dep_service_dct = services[dep_service]
37
+ out[dep_service] = dep_service_dct['image']
38
+
39
+ return out
40
+
41
+
42
+ ##
43
+
44
+
45
+ class DockerComposeRun(ExitStacked):
46
+ @dc.dataclass(frozen=True)
47
+ class Config:
48
+ compose_file: str
49
+ service: str
50
+
51
+ image: str
52
+
53
+ run_cmd: ta.Sequence[str]
54
+
55
+ #
56
+
57
+ run_options: ta.Optional[ta.Sequence[str]] = None
58
+
59
+ cwd: ta.Optional[str] = None
60
+
61
+ #
62
+
63
+ def __post_init__(self) -> None:
64
+ check.not_isinstance(self.run_cmd, str)
65
+
66
+ check.not_isinstance(self.run_options, str)
67
+
68
+ def __init__(self, cfg: Config) -> None:
69
+ super().__init__()
70
+
71
+ self._cfg = cfg
72
+
73
+ self._subprocess_kwargs = {
74
+ **(dict(cwd=self._cfg.cwd) if self._cfg.cwd is not None else {}),
75
+ }
76
+
77
+ #
78
+
79
+ @property
80
+ def image_tag(self) -> str:
81
+ pfx = 'sha256:'
82
+ if (image := self._cfg.image).startswith(pfx):
83
+ image = image[len(pfx):]
84
+
85
+ return f'{self._cfg.service}:{image}'
86
+
87
+ @cached_nullary
88
+ def tag_image(self) -> str:
89
+ image_tag = self.image_tag
90
+
91
+ subprocesses.check_call(
92
+ 'docker',
93
+ 'tag',
94
+ self._cfg.image,
95
+ image_tag,
96
+ **self._subprocess_kwargs,
97
+ )
98
+
99
+ def delete_tag() -> None:
100
+ subprocesses.check_call(
101
+ 'docker',
102
+ 'rmi',
103
+ image_tag,
104
+ **self._subprocess_kwargs,
105
+ )
106
+
107
+ self._enter_context(defer(delete_tag)) # noqa
108
+
109
+ return image_tag
110
+
111
+ #
112
+
113
+ def _rewrite_compose_dct(self, in_dct: ta.Dict[str, ta.Any]) -> ta.Dict[str, ta.Any]:
114
+ out = dict(in_dct)
115
+
116
+ #
117
+
118
+ in_services = in_dct['services']
119
+ out['services'] = out_services = {}
120
+
121
+ #
122
+
123
+ in_service: dict = in_services[self._cfg.service]
124
+ out_services[self._cfg.service] = out_service = dict(in_service)
125
+
126
+ out_service['image'] = self.image_tag
127
+
128
+ for k in ['build', 'platform']:
129
+ if k in out_service:
130
+ del out_service[k]
131
+
132
+ out_service['links'] = [
133
+ f'{l}:{l}' if ':' not in l else l
134
+ for l in out_service.get('links', [])
135
+ ]
136
+
137
+ #
138
+
139
+ depends_on = in_service.get('depends_on', [])
140
+
141
+ for dep_service, in_dep_service_dct in list(in_services.items()):
142
+ if dep_service not in depends_on:
143
+ continue
144
+
145
+ out_dep_service: dict = dict(in_dep_service_dct)
146
+ out_services[dep_service] = out_dep_service
147
+
148
+ out_dep_service['ports'] = []
149
+
150
+ #
151
+
152
+ return out
153
+
154
+ @cached_nullary
155
+ def rewrite_compose_file(self) -> str:
156
+ in_dct = read_yaml_file(self._cfg.compose_file)
157
+
158
+ out_dct = self._rewrite_compose_dct(in_dct)
159
+
160
+ #
161
+
162
+ out_compose_file = make_temp_file()
163
+ self._enter_context(defer(lambda: os.unlink(out_compose_file))) # noqa
164
+
165
+ compose_json = json_dumps_pretty(out_dct)
166
+
167
+ with open(out_compose_file, 'w') as f:
168
+ f.write(compose_json)
169
+
170
+ return out_compose_file
171
+
172
+ #
173
+
174
+ def run(self) -> None:
175
+ self.tag_image()
176
+
177
+ compose_file = self.rewrite_compose_file()
178
+
179
+ try:
180
+ subprocesses.check_call(
181
+ 'docker',
182
+ 'compose',
183
+ '-f', compose_file,
184
+ 'run',
185
+ '--rm',
186
+ *self._cfg.run_options or [],
187
+ self._cfg.service,
188
+ *self._cfg.run_cmd,
189
+ **self._subprocess_kwargs,
190
+ )
191
+
192
+ finally:
193
+ subprocesses.check_call(
194
+ 'docker',
195
+ 'compose',
196
+ '-f', compose_file,
197
+ 'down',
198
+ )
omdev/ci/dockertars.py ADDED
@@ -0,0 +1,138 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - some less stupid Dockerfile hash
6
+ - doesn't change too much though
7
+ """
8
+ import contextlib
9
+ import json
10
+ import os.path
11
+ import tarfile
12
+ import typing as ta
13
+
14
+ from omlish.lite.check import check
15
+ from omlish.lite.contextmanagers import defer
16
+ from omlish.subprocesses import subprocesses
17
+
18
+ from .utils import make_temp_file
19
+ from .utils import sha256_str
20
+
21
+
22
+ ##
23
+
24
+
25
+ def build_docker_file_hash(docker_file: str) -> str:
26
+ with open(docker_file) as f:
27
+ contents = f.read()
28
+
29
+ return sha256_str(contents)
30
+
31
+
32
+ ##
33
+
34
+
35
+ def read_docker_tar_image_tag(tar_file: str) -> str:
36
+ with tarfile.open(tar_file) as tf:
37
+ with contextlib.closing(check.not_none(tf.extractfile('manifest.json'))) as mf:
38
+ m = mf.read()
39
+
40
+ manifests = json.loads(m.decode('utf-8'))
41
+ manifest = check.single(manifests)
42
+ tag = check.non_empty_str(check.single(manifest['RepoTags']))
43
+ return tag
44
+
45
+
46
+ def read_docker_tar_image_id(tar_file: str) -> str:
47
+ with tarfile.open(tar_file) as tf:
48
+ with contextlib.closing(check.not_none(tf.extractfile('index.json'))) as mf:
49
+ i = mf.read()
50
+
51
+ index = json.loads(i.decode('utf-8'))
52
+ manifest = check.single(index['manifests'])
53
+ image_id = check.non_empty_str(manifest['digest'])
54
+ return image_id
55
+
56
+
57
+ ##
58
+
59
+
60
+ def is_docker_image_present(image: str) -> bool:
61
+ out = subprocesses.check_output(
62
+ 'docker',
63
+ 'images',
64
+ '--format', 'json',
65
+ image,
66
+ )
67
+
68
+ out_s = out.decode('utf-8').strip()
69
+ if not out_s:
70
+ return False
71
+
72
+ json.loads(out_s) # noqa
73
+ return True
74
+
75
+
76
+ ##
77
+
78
+
79
+ def pull_docker_tar(
80
+ image: str,
81
+ tar_file: str,
82
+ ) -> None:
83
+ subprocesses.check_call(
84
+ 'docker',
85
+ 'pull',
86
+ image,
87
+ )
88
+
89
+ subprocesses.check_call(
90
+ 'docker',
91
+ 'save',
92
+ image,
93
+ '-o', tar_file,
94
+ )
95
+
96
+
97
+ def build_docker_tar(
98
+ docker_file: str,
99
+ tar_file: str,
100
+ *,
101
+ cwd: ta.Optional[str] = None,
102
+ ) -> str:
103
+ id_file = make_temp_file()
104
+ with defer(lambda: os.unlink(id_file)):
105
+ subprocesses.check_call(
106
+ 'docker',
107
+ 'build',
108
+ '-f', os.path.abspath(docker_file),
109
+ '--iidfile', id_file,
110
+ '--squash',
111
+ '.',
112
+ **(dict(cwd=cwd) if cwd is not None else {}),
113
+ )
114
+
115
+ with open(id_file) as f:
116
+ image_id = check.single(f.read().strip().splitlines()).strip()
117
+
118
+ subprocesses.check_call(
119
+ 'docker',
120
+ 'save',
121
+ image_id,
122
+ '-o', tar_file,
123
+ )
124
+
125
+ return image_id
126
+
127
+
128
+ ##
129
+
130
+
131
+ def load_docker_tar(
132
+ tar_file: str,
133
+ ) -> None:
134
+ subprocesses.check_call(
135
+ 'docker',
136
+ 'load',
137
+ '-i', tar_file,
138
+ )
@@ -0,0 +1,79 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - pip compile lol
6
+ - but still support git+ stuff
7
+ - req.txt format aware hash
8
+ - more than just whitespace
9
+ - pyproject req rewriting
10
+ - download_requirements bootstrap off prev? not worth the dl?
11
+ - big deps (torch) change less, probably worth it
12
+ - follow embedded -r automatically like pyp
13
+ """
14
+ import itertools
15
+ import os.path
16
+ import shutil
17
+ import tempfile
18
+ import typing as ta
19
+
20
+ from omlish.lite.check import check
21
+ from omlish.lite.contextmanagers import defer
22
+ from omlish.subprocesses import subprocesses
23
+
24
+ from .utils import sha256_str
25
+
26
+
27
+ ##
28
+
29
+
30
+ def build_requirements_hash(
31
+ requirements_txts: ta.Sequence[str],
32
+ ) -> str:
33
+ txt_file_contents: dict = {}
34
+
35
+ for txt_file in requirements_txts:
36
+ txt_file_name = os.path.basename(txt_file)
37
+ check.not_in(txt_file_name, txt_file_contents)
38
+ with open(txt_file) as f:
39
+ txt_contents = f.read()
40
+ txt_file_contents[txt_file_name] = txt_contents
41
+
42
+ #
43
+
44
+ lines = []
45
+ for txt_file, txt_contents in sorted(txt_file_contents.items()):
46
+ txt_hash = sha256_str(txt_contents)
47
+ lines.append(f'{txt_file}={txt_hash}')
48
+
49
+ return sha256_str('\n'.join(lines))
50
+
51
+
52
+ ##
53
+
54
+
55
+ def download_requirements(
56
+ image: str,
57
+ requirements_dir: str,
58
+ requirements_txts: ta.Sequence[str],
59
+ ) -> None:
60
+ requirements_txt_dir = tempfile.mkdtemp()
61
+ with defer(lambda: shutil.rmtree(requirements_txt_dir)):
62
+ for rt in requirements_txts:
63
+ shutil.copyfile(rt, os.path.join(requirements_txt_dir, os.path.basename(rt)))
64
+
65
+ subprocesses.check_call(
66
+ 'docker',
67
+ 'run',
68
+ '-i',
69
+ '-v', f'{os.path.abspath(requirements_dir)}:/requirements',
70
+ '-v', f'{requirements_txt_dir}:/requirements_txt',
71
+ image,
72
+ 'pip',
73
+ 'download',
74
+ '-d', '/requirements',
75
+ *itertools.chain.from_iterable([
76
+ ['-r', f'/requirements_txt/{os.path.basename(rt)}']
77
+ for rt in requirements_txts
78
+ ]),
79
+ )
omdev/ci/utils.py ADDED
@@ -0,0 +1,32 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import hashlib
4
+ import os.path
5
+ import tempfile
6
+ import typing as ta
7
+
8
+
9
+ ##
10
+
11
+
12
+ def make_temp_file() -> str:
13
+ file_fd, file = tempfile.mkstemp()
14
+ os.close(file_fd)
15
+ return file
16
+
17
+
18
+ ##
19
+
20
+
21
+ def read_yaml_file(yaml_file: str) -> ta.Any:
22
+ yaml = __import__('yaml')
23
+
24
+ with open(yaml_file) as f:
25
+ return yaml.safe_load(f)
26
+
27
+
28
+ ##
29
+
30
+
31
+ def sha256_str(s: str) -> str:
32
+ return hashlib.sha256(s.encode('utf-8')).hexdigest()
omdev/interp/cli.py CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env python3
2
1
  # @omlish-amalg ../scripts/interp.py
3
2
  # ruff: noqa: UP007
4
3
  """
@@ -104,7 +104,8 @@ class SystemInterpProvider(InterpProvider):
104
104
  async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
105
105
  if not self._options.inspect:
106
106
  s = os.path.basename(exe)
107
- s = s.removeprefix('python')
107
+ if s.startswith('python'): # noqa
108
+ s = s[len('python'):]
108
109
  if '.' in s:
109
110
  try:
110
111
  return InterpVersion.parse(s)
omdev/pycharm/cli.py CHANGED
@@ -88,7 +88,7 @@ class Cli(ap.Cli):
88
88
 
89
89
  @ap.cmd(
90
90
  ap.arg('dir', nargs='?'),
91
- ap.arg('--clion', action='store_true'),
91
+ ap.arg('-c', '--clion', action='store_true'),
92
92
  )
93
93
  def open(self) -> None:
94
94
  dir = os.path.abspath(self.args.dir or '.') # noqa
omdev/pyproject/cli.py CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env python3
2
1
  # @omlish-amalg ../scripts/pyproject.py
3
2
  # ruff: noqa: UP006 UP007
4
3
  """
omdev/revisions.py CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env python3
2
1
  """
3
2
  TODO:
4
3
  - omlish-lite, move to pyproject/