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/.manifests.json
CHANGED
omdev/amalg/main.py
CHANGED
@@ -28,6 +28,7 @@ Targets:
|
|
28
28
|
import argparse
|
29
29
|
import logging
|
30
30
|
import os.path
|
31
|
+
import stat
|
31
32
|
import typing as ta
|
32
33
|
|
33
34
|
from omlish import check
|
@@ -60,7 +61,15 @@ def _gen_one(
|
|
60
61
|
if output_path is not None:
|
61
62
|
with open(output_path, 'w') as f:
|
62
63
|
f.write(src)
|
63
|
-
|
64
|
+
|
65
|
+
src_mode = os.stat(input_path).st_mode
|
66
|
+
out_mode = (
|
67
|
+
src_mode
|
68
|
+
| (stat.S_IXUSR if src_mode & stat.S_IRUSR else 0)
|
69
|
+
| (stat.S_IXGRP if src_mode & stat.S_IRGRP else 0)
|
70
|
+
| (stat.S_IXOTH if src_mode & stat.S_IROTH else 0)
|
71
|
+
)
|
72
|
+
os.chmod(output_path, out_mode)
|
64
73
|
|
65
74
|
else:
|
66
75
|
print(src)
|
omdev/cc/cdeps.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import dataclasses as dc
|
2
|
+
import tomllib
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from omlish import cached
|
6
|
+
from omlish import lang
|
7
|
+
from omlish import marshal as msh
|
8
|
+
|
9
|
+
|
10
|
+
@dc.dataclass(frozen=True)
|
11
|
+
class Cdep:
|
12
|
+
@dc.dataclass(frozen=True)
|
13
|
+
class Git:
|
14
|
+
url: str
|
15
|
+
rev: str
|
16
|
+
|
17
|
+
subtrees: ta.Sequence[str] | None = None
|
18
|
+
|
19
|
+
git: Git
|
20
|
+
|
21
|
+
#
|
22
|
+
|
23
|
+
include: ta.Sequence[str] | None = None
|
24
|
+
|
25
|
+
#
|
26
|
+
|
27
|
+
@dc.dataclass(frozen=True)
|
28
|
+
class Cmake:
|
29
|
+
fetch_content_url: str | None = None
|
30
|
+
|
31
|
+
cmake: Cmake | None = None
|
32
|
+
|
33
|
+
|
34
|
+
@cached.function
|
35
|
+
def load_cdeps() -> ta.Mapping[str, Cdep]:
|
36
|
+
src = lang.get_relative_resources(globals=globals())['cdeps.toml'].read_text()
|
37
|
+
dct = tomllib.loads(src)
|
38
|
+
return msh.unmarshal(dct.get('deps', {}), ta.Mapping[str, Cdep]) # type: ignore
|
omdev/cc/cdeps.toml
CHANGED
@@ -3,9 +3,12 @@ include = ['single_include']
|
|
3
3
|
|
4
4
|
[deps.json.git]
|
5
5
|
url = 'https://github.com/nlohmann/json'
|
6
|
-
rev = '
|
6
|
+
rev = '9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03'
|
7
7
|
subtrees = ['single_include']
|
8
8
|
|
9
|
+
[deps.json.cmake]
|
10
|
+
fetch_content_url = 'https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz'
|
11
|
+
|
9
12
|
#
|
10
13
|
|
11
14
|
[deps.pybind11]
|
omdev/cc/cli.py
CHANGED
@@ -19,45 +19,22 @@ TODO:
|
|
19
19
|
- cext interop
|
20
20
|
- gen cmake
|
21
21
|
"""
|
22
|
-
import dataclasses as dc
|
23
22
|
import os
|
24
23
|
import shlex
|
25
24
|
import shutil
|
26
25
|
import subprocess
|
27
26
|
import tempfile
|
28
|
-
import tomllib
|
29
27
|
import typing as ta
|
30
28
|
|
31
|
-
from omlish import cached
|
32
29
|
from omlish import check
|
33
|
-
from omlish import lang
|
34
30
|
from omlish import marshal as msh
|
35
31
|
from omlish.argparse import all as ap
|
36
32
|
from omlish.formats import json
|
37
33
|
|
38
34
|
from .. import magic
|
39
35
|
from ..cache import data as dcache
|
40
|
-
|
41
|
-
|
42
|
-
@dc.dataclass(frozen=True)
|
43
|
-
class Cdep:
|
44
|
-
@dc.dataclass(frozen=True)
|
45
|
-
class Git:
|
46
|
-
url: str
|
47
|
-
rev: str
|
48
|
-
|
49
|
-
subtrees: ta.Sequence[str] | None = None
|
50
|
-
|
51
|
-
git: Git
|
52
|
-
|
53
|
-
include: ta.Sequence[str] | None = None
|
54
|
-
|
55
|
-
|
56
|
-
@cached.function
|
57
|
-
def load_cdeps() -> ta.Mapping[str, Cdep]:
|
58
|
-
src = lang.get_relative_resources(globals=globals())['cdeps.toml'].read_text()
|
59
|
-
dct = tomllib.loads(src)
|
60
|
-
return msh.unmarshal(dct.get('deps', {}), ta.Mapping[str, Cdep]) # type: ignore
|
36
|
+
from .cdeps import Cdep
|
37
|
+
from .cdeps import load_cdeps
|
61
38
|
|
62
39
|
|
63
40
|
class Cli(ap.Cli):
|
omdev/ci/__init__.py
ADDED
File without changes
|
omdev/ci/__main__.py
ADDED
omdev/ci/cache.py
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import abc
|
4
|
+
import os.path
|
5
|
+
import shutil
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
|
9
|
+
#
|
10
|
+
|
11
|
+
|
12
|
+
@abc.abstractmethod
|
13
|
+
class FileCache(abc.ABC):
|
14
|
+
@abc.abstractmethod
|
15
|
+
def get_file(self, name: str) -> ta.Optional[str]:
|
16
|
+
raise NotImplementedError
|
17
|
+
|
18
|
+
@abc.abstractmethod
|
19
|
+
def put_file(self, name: str) -> ta.Optional[str]:
|
20
|
+
raise NotImplementedError
|
21
|
+
|
22
|
+
|
23
|
+
#
|
24
|
+
|
25
|
+
|
26
|
+
class DirectoryFileCache(FileCache):
|
27
|
+
def __init__(self, dir: str) -> None: # noqa
|
28
|
+
super().__init__()
|
29
|
+
|
30
|
+
self._dir = dir
|
31
|
+
|
32
|
+
def get_file(self, name: str) -> ta.Optional[str]:
|
33
|
+
file_path = os.path.join(self._dir, name)
|
34
|
+
if not os.path.exists(file_path):
|
35
|
+
return None
|
36
|
+
return file_path
|
37
|
+
|
38
|
+
def put_file(self, file_path: str) -> None:
|
39
|
+
os.makedirs(self._dir, exist_ok=True)
|
40
|
+
cache_file_path = os.path.join(self._dir, os.path.basename(file_path))
|
41
|
+
shutil.copyfile(file_path, cache_file_path)
|
omdev/ci/ci.py
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import dataclasses as dc
|
4
|
+
import os.path
|
5
|
+
import shutil
|
6
|
+
import tarfile
|
7
|
+
import tempfile
|
8
|
+
import typing as ta
|
9
|
+
|
10
|
+
from omlish.lite.cached import cached_nullary
|
11
|
+
from omlish.lite.check import check
|
12
|
+
from omlish.lite.contextmanagers import ExitStacked
|
13
|
+
from omlish.lite.contextmanagers import defer
|
14
|
+
|
15
|
+
from .cache import FileCache
|
16
|
+
from .compose import DockerComposeRun
|
17
|
+
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
|
24
|
+
from .requirements import build_requirements_hash
|
25
|
+
from .requirements import download_requirements
|
26
|
+
|
27
|
+
|
28
|
+
##
|
29
|
+
|
30
|
+
|
31
|
+
class Ci(ExitStacked):
|
32
|
+
FILE_NAME_HASH_LEN = 16
|
33
|
+
|
34
|
+
@dc.dataclass(frozen=True)
|
35
|
+
class Config:
|
36
|
+
project_dir: str
|
37
|
+
|
38
|
+
docker_file: str
|
39
|
+
|
40
|
+
compose_file: str
|
41
|
+
service: str
|
42
|
+
|
43
|
+
requirements_txts: ta.Optional[ta.Sequence[str]] = None
|
44
|
+
|
45
|
+
def __post_init__(self) -> None:
|
46
|
+
check.not_isinstance(self.requirements_txts, str)
|
47
|
+
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
cfg: Config,
|
51
|
+
*,
|
52
|
+
file_cache: ta.Optional[FileCache] = None,
|
53
|
+
) -> None:
|
54
|
+
super().__init__()
|
55
|
+
|
56
|
+
self._cfg = cfg
|
57
|
+
self._file_cache = file_cache
|
58
|
+
|
59
|
+
#
|
60
|
+
|
61
|
+
def load_docker_image(self, image: str) -> None:
|
62
|
+
if is_docker_image_present(image):
|
63
|
+
return
|
64
|
+
|
65
|
+
dep_suffix = image
|
66
|
+
for c in '/:.-_':
|
67
|
+
dep_suffix = dep_suffix.replace(c, '-')
|
68
|
+
|
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)
|
73
|
+
return
|
74
|
+
|
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)
|
78
|
+
|
79
|
+
pull_docker_tar(
|
80
|
+
image,
|
81
|
+
temp_tar_file,
|
82
|
+
)
|
83
|
+
|
84
|
+
if self._file_cache is not None:
|
85
|
+
self._file_cache.put_file(temp_tar_file)
|
86
|
+
|
87
|
+
@cached_nullary
|
88
|
+
def load_compose_service_dependencies(self) -> None:
|
89
|
+
deps = get_compose_service_dependencies(
|
90
|
+
self._cfg.compose_file,
|
91
|
+
self._cfg.service,
|
92
|
+
)
|
93
|
+
|
94
|
+
for dep_image in deps.values():
|
95
|
+
self.load_docker_image(dep_image)
|
96
|
+
|
97
|
+
#
|
98
|
+
|
99
|
+
@cached_nullary
|
100
|
+
def build_ci_image(self) -> str:
|
101
|
+
docker_file_hash = build_docker_file_hash(self._cfg.docker_file)[:self.FILE_NAME_HASH_LEN]
|
102
|
+
|
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
|
109
|
+
|
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)
|
113
|
+
|
114
|
+
image_id = build_docker_tar(
|
115
|
+
self._cfg.docker_file,
|
116
|
+
temp_tar_file,
|
117
|
+
cwd=self._cfg.project_dir,
|
118
|
+
)
|
119
|
+
|
120
|
+
if self._file_cache is not None:
|
121
|
+
self._file_cache.put_file(temp_tar_file)
|
122
|
+
|
123
|
+
return image_id
|
124
|
+
|
125
|
+
#
|
126
|
+
|
127
|
+
@cached_nullary
|
128
|
+
def build_requirements_dir(self) -> str:
|
129
|
+
requirements_txts = check.not_none(self._cfg.requirements_txts)
|
130
|
+
|
131
|
+
requirements_hash = build_requirements_hash(requirements_txts)[:self.FILE_NAME_HASH_LEN]
|
132
|
+
|
133
|
+
tar_file_name = f'requirements-{requirements_hash}.tar'
|
134
|
+
|
135
|
+
temp_dir = tempfile.mkdtemp()
|
136
|
+
self._enter_context(defer(lambda: shutil.rmtree(temp_dir))) # noqa
|
137
|
+
|
138
|
+
if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_name)):
|
139
|
+
with tarfile.open(cache_tar_file) as tar:
|
140
|
+
tar.extractall(path=temp_dir) # noqa
|
141
|
+
|
142
|
+
return temp_dir
|
143
|
+
|
144
|
+
temp_requirements_dir = os.path.join(temp_dir, 'requirements')
|
145
|
+
os.makedirs(temp_requirements_dir)
|
146
|
+
|
147
|
+
download_requirements(
|
148
|
+
self.build_ci_image(),
|
149
|
+
temp_requirements_dir,
|
150
|
+
requirements_txts,
|
151
|
+
)
|
152
|
+
|
153
|
+
if self._file_cache is not None:
|
154
|
+
temp_tar_file = os.path.join(temp_dir, tar_file_name)
|
155
|
+
|
156
|
+
with tarfile.open(temp_tar_file, 'w') as tar:
|
157
|
+
for requirement_file in os.listdir(temp_requirements_dir):
|
158
|
+
tar.add(
|
159
|
+
os.path.join(temp_requirements_dir, requirement_file),
|
160
|
+
arcname=requirement_file,
|
161
|
+
)
|
162
|
+
|
163
|
+
self._file_cache.put_file(temp_tar_file)
|
164
|
+
|
165
|
+
return temp_requirements_dir
|
166
|
+
|
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()
|
175
|
+
|
176
|
+
#
|
177
|
+
|
178
|
+
setup_cmds = [
|
179
|
+
'pip install --root-user-action ignore --find-links /requirements --no-index uv',
|
180
|
+
(
|
181
|
+
'uv pip install --system --find-links /requirements ' +
|
182
|
+
' '.join(f'-r /project/{rf}' for rf in self._cfg.requirements_txts or [])
|
183
|
+
),
|
184
|
+
]
|
185
|
+
|
186
|
+
#
|
187
|
+
|
188
|
+
test_cmds = [
|
189
|
+
'(cd /project && python3 -m pytest -svv test.py)',
|
190
|
+
]
|
191
|
+
|
192
|
+
#
|
193
|
+
|
194
|
+
bash_src = ' && '.join([
|
195
|
+
*setup_cmds,
|
196
|
+
*test_cmds,
|
197
|
+
])
|
198
|
+
|
199
|
+
with DockerComposeRun(DockerComposeRun.Config(
|
200
|
+
compose_file=self._cfg.compose_file,
|
201
|
+
service=self._cfg.service,
|
202
|
+
|
203
|
+
image=ci_image,
|
204
|
+
|
205
|
+
run_cmd=['bash', '-c', bash_src],
|
206
|
+
|
207
|
+
run_options=[
|
208
|
+
'-v', f'{os.path.abspath(self._cfg.project_dir)}:/project',
|
209
|
+
'-v', f'{os.path.abspath(requirements_dir)}:/requirements',
|
210
|
+
],
|
211
|
+
|
212
|
+
cwd=self._cfg.project_dir,
|
213
|
+
)) as ci_compose_run:
|
214
|
+
ci_compose_run.run()
|
omdev/ci/cli.py
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
# @omlish-amalg ../scripts/ci.py
|
2
|
+
# ruff: noqa: UP006 UP007
|
3
|
+
# @omlish-lite
|
4
|
+
"""
|
5
|
+
Inputs:
|
6
|
+
- requirements.txt
|
7
|
+
- ci.Dockerfile
|
8
|
+
- compose.yml
|
9
|
+
|
10
|
+
==
|
11
|
+
|
12
|
+
./python -m ci run --cache-dir ci/cache ci/project omlish-ci
|
13
|
+
"""
|
14
|
+
import asyncio
|
15
|
+
import os.path
|
16
|
+
import sys
|
17
|
+
import typing as ta
|
18
|
+
|
19
|
+
from omlish.argparse.cli import ArgparseCli
|
20
|
+
from omlish.argparse.cli import argparse_arg
|
21
|
+
from omlish.argparse.cli import argparse_cmd
|
22
|
+
from omlish.lite.check import check
|
23
|
+
|
24
|
+
from .cache import DirectoryFileCache
|
25
|
+
from .cache import FileCache
|
26
|
+
from .ci import Ci
|
27
|
+
from .compose import get_compose_service_dependencies
|
28
|
+
from .requirements import build_requirements_hash
|
29
|
+
|
30
|
+
|
31
|
+
##
|
32
|
+
|
33
|
+
|
34
|
+
class CiCli(ArgparseCli):
|
35
|
+
#
|
36
|
+
|
37
|
+
@argparse_cmd(
|
38
|
+
argparse_arg('requirements-txt', nargs='+'),
|
39
|
+
)
|
40
|
+
def print_requirements_hash(self) -> None:
|
41
|
+
requirements_txts = self.args.requirements_txt
|
42
|
+
|
43
|
+
print(build_requirements_hash(requirements_txts))
|
44
|
+
|
45
|
+
#
|
46
|
+
|
47
|
+
@argparse_cmd(
|
48
|
+
argparse_arg('compose-file'),
|
49
|
+
argparse_arg('service'),
|
50
|
+
)
|
51
|
+
def dump_compose_deps(self) -> None:
|
52
|
+
compose_file = self.args.compose_file
|
53
|
+
service = self.args.service
|
54
|
+
|
55
|
+
print(get_compose_service_dependencies(
|
56
|
+
compose_file,
|
57
|
+
service,
|
58
|
+
))
|
59
|
+
|
60
|
+
#
|
61
|
+
|
62
|
+
@argparse_cmd(
|
63
|
+
argparse_arg('project-dir'),
|
64
|
+
argparse_arg('service'),
|
65
|
+
argparse_arg('--docker-file'),
|
66
|
+
argparse_arg('--compose-file'),
|
67
|
+
argparse_arg('-r', '--requirements-txt', action='append'),
|
68
|
+
argparse_arg('--cache-dir'),
|
69
|
+
)
|
70
|
+
async def run(self) -> None:
|
71
|
+
await asyncio.sleep(1)
|
72
|
+
|
73
|
+
project_dir = self.args.project_dir
|
74
|
+
docker_file = self.args.docker_file
|
75
|
+
compose_file = self.args.compose_file
|
76
|
+
service = self.args.service
|
77
|
+
requirements_txts = self.args.requirements_txt
|
78
|
+
cache_dir = self.args.cache_dir
|
79
|
+
|
80
|
+
#
|
81
|
+
|
82
|
+
check.state(os.path.isdir(project_dir))
|
83
|
+
|
84
|
+
#
|
85
|
+
|
86
|
+
def find_alt_file(*alts: str) -> ta.Optional[str]:
|
87
|
+
for alt in alts:
|
88
|
+
alt_file = os.path.join(project_dir, alt)
|
89
|
+
if os.path.isfile(alt_file):
|
90
|
+
return alt_file
|
91
|
+
return None
|
92
|
+
|
93
|
+
if docker_file is None:
|
94
|
+
docker_file = find_alt_file(
|
95
|
+
'docker/ci/Dockerfile',
|
96
|
+
'docker/ci.Dockerfile',
|
97
|
+
'ci.Dockerfile',
|
98
|
+
'Dockerfile',
|
99
|
+
)
|
100
|
+
check.state(os.path.isfile(docker_file))
|
101
|
+
|
102
|
+
if compose_file is None:
|
103
|
+
compose_file = find_alt_file(
|
104
|
+
'docker/compose.yml',
|
105
|
+
'compose.yml',
|
106
|
+
)
|
107
|
+
check.state(os.path.isfile(compose_file))
|
108
|
+
|
109
|
+
if not requirements_txts:
|
110
|
+
requirements_txts = []
|
111
|
+
for rf in [
|
112
|
+
'requirements.txt',
|
113
|
+
'requirements-dev.txt',
|
114
|
+
'requirements-ci.txt',
|
115
|
+
]:
|
116
|
+
if os.path.exists(os.path.join(project_dir, rf)):
|
117
|
+
requirements_txts.append(rf)
|
118
|
+
else:
|
119
|
+
for rf in requirements_txts:
|
120
|
+
check.state(os.path.isfile(rf))
|
121
|
+
|
122
|
+
#
|
123
|
+
|
124
|
+
file_cache: ta.Optional[FileCache] = None
|
125
|
+
if cache_dir is not None:
|
126
|
+
if not os.path.exists(cache_dir):
|
127
|
+
os.makedirs(cache_dir)
|
128
|
+
check.state(os.path.isdir(cache_dir))
|
129
|
+
file_cache = DirectoryFileCache(cache_dir)
|
130
|
+
|
131
|
+
#
|
132
|
+
|
133
|
+
with Ci(
|
134
|
+
Ci.Config(
|
135
|
+
project_dir=project_dir,
|
136
|
+
docker_file=docker_file,
|
137
|
+
compose_file=compose_file,
|
138
|
+
service=service,
|
139
|
+
requirements_txts=requirements_txts,
|
140
|
+
),
|
141
|
+
file_cache=file_cache,
|
142
|
+
) as ci:
|
143
|
+
ci.run()
|
144
|
+
|
145
|
+
|
146
|
+
async def _async_main() -> ta.Optional[int]:
|
147
|
+
return await CiCli().async_cli_run()
|
148
|
+
|
149
|
+
|
150
|
+
def _main() -> None:
|
151
|
+
sys.exit(rc if isinstance(rc := asyncio.run(_async_main()), int) else 0)
|
152
|
+
|
153
|
+
|
154
|
+
if __name__ == '__main__':
|
155
|
+
_main()
|