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/.manifests.json +1 -1
- omdev/amalg/main.py +10 -1
- omdev/cc/cdeps.py +38 -0
- omdev/cc/cdeps.toml +4 -1
- omdev/cc/cli.py +2 -25
- omdev/ci/__init__.py +0 -0
- omdev/ci/__main__.py +4 -0
- omdev/ci/cache.py +41 -0
- omdev/ci/ci.py +214 -0
- omdev/ci/cli.py +155 -0
- omdev/ci/compose.py +198 -0
- omdev/ci/dockertars.py +138 -0
- omdev/ci/requirements.py +79 -0
- omdev/ci/utils.py +32 -0
- omdev/interp/cli.py +0 -1
- omdev/interp/providers/system.py +2 -1
- omdev/pycharm/cli.py +1 -1
- omdev/pyproject/cli.py +0 -1
- omdev/revisions.py +0 -1
- omdev/scripts/ci.py +2183 -0
- omdev/scripts/interp.py +19 -3
- omdev/scripts/pyproject.py +19 -3
- omdev/tools/pawk/pawk.py +0 -1
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev210.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev210.dist-info}/RECORD +29 -18
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev210.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev210.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev210.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev209.dist-info → omdev-0.0.0.dev210.dist-info}/top_level.txt +0 -0
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
|
+
)
|
omdev/ci/requirements.py
ADDED
@@ -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
omdev/interp/providers/system.py
CHANGED
@@ -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
|
-
|
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
omdev/pyproject/cli.py
CHANGED
omdev/revisions.py
CHANGED