omdev 0.0.0.dev209__py3-none-any.whl → 0.0.0.dev210__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/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/