omdev 0.0.0.dev208__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/.manifests.json +1 -1
- omdev/amalg/main.py +10 -1
- omdev/cc/cdeps.py +38 -0
- omdev/cc/cdeps.toml +30 -0
- 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.dev208.dist-info → omdev-0.0.0.dev210.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev208.dist-info → omdev-0.0.0.dev210.dist-info}/RECORD +29 -17
- {omdev-0.0.0.dev208.dist-info → omdev-0.0.0.dev210.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev208.dist-info → omdev-0.0.0.dev210.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev208.dist-info → omdev-0.0.0.dev210.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev208.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