xflow.framework 0.1.0__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.
- xflow/framework/__init__.py +0 -0
- xflow/framework/common.py +14 -0
- xflow/framework/container.py +292 -0
- xflow/framework/env.py +72 -0
- xflow/framework/errors.py +33 -0
- xflow/framework/main.py +112 -0
- xflow/framework/node.py +392 -0
- xflow/framework/pipeline.py +189 -0
- xflow/framework/ssh.py +372 -0
- xflow/framework/statics/initdir/.gitignore +215 -0
- xflow/framework/statics/initdir/README.md +0 -0
- xflow/framework/statics/initdir/env.yml +41 -0
- xflow/framework/statics/initdir/pipelines/__init__.py +0 -0
- xflow/framework/statics/initdir/pipelines/example.py +60 -0
- xflow/framework/statics/initdir/pipelines/template.py +32 -0
- xflow/framework/statics/initdir/requirements.txt +1 -0
- xflow/framework/statics/initdir/scripts/.gitkeep +0 -0
- xflow/framework/statics/initdir/workdir/.gitkeep +0 -0
- xflow/framework/utils.py +59 -0
- xflow/framework/version.py +4 -0
- xflow_framework-0.1.0.dist-info/METADATA +43 -0
- xflow_framework-0.1.0.dist-info/RECORD +26 -0
- xflow_framework-0.1.0.dist-info/WHEEL +5 -0
- xflow_framework-0.1.0.dist-info/entry_points.txt +2 -0
- xflow_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- xflow_framework-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
公共变量/常量。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
XFLOW_DIR: Path = Path(__file__).parent
|
|
11
|
+
|
|
12
|
+
STATICS_DIR = XFLOW_DIR.joinpath('statics')
|
|
13
|
+
|
|
14
|
+
INIT_DIR = STATICS_DIR.joinpath('initdir')
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
容器模块。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import tarfile
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from typing import Generator, Optional, Union, Dict
|
|
12
|
+
from select import select
|
|
13
|
+
from socket import SocketIO
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from pathlib import Path, PurePosixPath
|
|
16
|
+
from functools import cached_property
|
|
17
|
+
|
|
18
|
+
from decorator import decorator
|
|
19
|
+
from docker.client import DockerClient
|
|
20
|
+
from docker.models.containers import Container
|
|
21
|
+
from docker.tls import TLSConfig
|
|
22
|
+
|
|
23
|
+
from xflow.framework.errors import CommandError
|
|
24
|
+
from xflow.framework.ssh import CommandResult
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@decorator
|
|
28
|
+
def autopen(func, *args, **kwargs):
|
|
29
|
+
"""
|
|
30
|
+
自动连接器。
|
|
31
|
+
"""
|
|
32
|
+
conn: ContainerConnection = args[0]
|
|
33
|
+
conn.open()
|
|
34
|
+
return func(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContainerConnection(object):
|
|
38
|
+
"""
|
|
39
|
+
容器连接。
|
|
40
|
+
"""
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
ip: str,
|
|
44
|
+
port: int = 2375,
|
|
45
|
+
user: Optional[str] = None,
|
|
46
|
+
name: Optional[str] = None,
|
|
47
|
+
image: Optional[str] = None,
|
|
48
|
+
runargs: Optional[Dict] = None,
|
|
49
|
+
cacert: Optional[str] = None,
|
|
50
|
+
clientcert: Optional[str] = None,
|
|
51
|
+
clientkey: Optional[str] = None,
|
|
52
|
+
envs: Optional[Dict[str, str]] = None
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
:param ip: IP 地址。
|
|
56
|
+
:param port: Docker 服务端口。
|
|
57
|
+
:param user: 用户名。
|
|
58
|
+
:param name: 容器名,和 `image` 为互斥参数,必须且只能指定其中一个。
|
|
59
|
+
:param image: 镜像名,和 `name` 为互斥参数,必须且只能指定其中一个,
|
|
60
|
+
如果该参数指定,则每次连接用该镜像创建一个新容器。
|
|
61
|
+
:param runargs: 使用镜像启动容器时的参数。
|
|
62
|
+
:param cacert: CA 证书。
|
|
63
|
+
:param clientcert: 客户端证书,当指定 `clientkey` 参数时必须同时指定该参数。
|
|
64
|
+
:param clientkey: 客户端私钥,当指定 `clientcert` 参数时必须同时指定该参数。
|
|
65
|
+
:param envs: 连接的默认环境变量,其中:
|
|
66
|
+
`LANG` 默认值为 `en_US.UTF-8`。
|
|
67
|
+
`LANGUAGE` 默认值为 `en_US.UTF-8`。
|
|
68
|
+
"""
|
|
69
|
+
# 必须且只能指定 `name` 和 `image` 参数中的一个。
|
|
70
|
+
if (name == None and image == None) or \
|
|
71
|
+
(name != None and image != None):
|
|
72
|
+
raise ValueError('Must specify only one of `name` and `image`.')
|
|
73
|
+
# `clientcert` 指定的同时必须指定 `clientkey`,反之亦然。
|
|
74
|
+
if (clientcert != None and clientkey == None) or \
|
|
75
|
+
(clientcert == None and clientkey != None):
|
|
76
|
+
raise ValueError('`clientcert` must be specified along with `clientkey`, and vice versa.')
|
|
77
|
+
self._ip = ip
|
|
78
|
+
self._port = port
|
|
79
|
+
self._user = user
|
|
80
|
+
self._envs = envs or {}
|
|
81
|
+
self._name = name
|
|
82
|
+
self._image = image
|
|
83
|
+
self._runargs = runargs or {}
|
|
84
|
+
self._cacert = cacert
|
|
85
|
+
self._clientcert = clientcert
|
|
86
|
+
self._clientkey = clientkey
|
|
87
|
+
self._dockerclient: DockerClient = None
|
|
88
|
+
self._container: Container = None
|
|
89
|
+
self._cwd: str = ''
|
|
90
|
+
for k, v in {'LANG': 'en_US.UTF-8',
|
|
91
|
+
'LANGUAGE': 'en_US.UTF-8'}.items():
|
|
92
|
+
self._envs.setdefault(k, v)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def _connstr(self) -> str:
|
|
96
|
+
"""
|
|
97
|
+
连接字符串。
|
|
98
|
+
"""
|
|
99
|
+
if self._user:
|
|
100
|
+
prefix = f'docker://{self._user}@{self._ip}:{self._port}'
|
|
101
|
+
else:
|
|
102
|
+
prefix = f'docker://{self._ip}:{self._port}'
|
|
103
|
+
if self._name:
|
|
104
|
+
return f'{prefix}:{self._name}'
|
|
105
|
+
else: # 从镜像自动创建容器
|
|
106
|
+
if self._container:
|
|
107
|
+
# 容器已创建
|
|
108
|
+
return f'{prefix}:{self._image}->{self._container.name}'
|
|
109
|
+
else:
|
|
110
|
+
# 容器未创建
|
|
111
|
+
return f'{prefix}:{self._image}->...'
|
|
112
|
+
|
|
113
|
+
@cached_property
|
|
114
|
+
def _uid(self) -> int:
|
|
115
|
+
"""
|
|
116
|
+
用户 id。
|
|
117
|
+
"""
|
|
118
|
+
return int(self.exec('id -u'))
|
|
119
|
+
|
|
120
|
+
def open(self) -> None:
|
|
121
|
+
"""
|
|
122
|
+
开启连接。
|
|
123
|
+
"""
|
|
124
|
+
if self._container:
|
|
125
|
+
return
|
|
126
|
+
print(f'[{self._connstr}] Connecting...')
|
|
127
|
+
if self._cacert or self._clientcert:
|
|
128
|
+
tls = TLSConfig(
|
|
129
|
+
ca_cert=self._cacert,
|
|
130
|
+
verify=True if self._cacert else False,
|
|
131
|
+
client_cert=(self._clientcert, self._clientkey) if self._clientcert else None
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
tls = False
|
|
135
|
+
self._dockerclient = DockerClient(base_url=f'tcp://{self._ip}:{self._port}', tls=tls)
|
|
136
|
+
if self._name:
|
|
137
|
+
self._container = self._dockerclient.containers.get(self._name)
|
|
138
|
+
else:
|
|
139
|
+
self._container = self._dockerclient.containers.run(self._image, detach=True, **self._runargs)
|
|
140
|
+
print(f'[{self._connstr}] Connected')
|
|
141
|
+
|
|
142
|
+
def close(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
关闭连接。
|
|
145
|
+
"""
|
|
146
|
+
self._dockerclient.close()
|
|
147
|
+
|
|
148
|
+
def remove(self, force: bool = False) -> None:
|
|
149
|
+
"""
|
|
150
|
+
删除本容器。
|
|
151
|
+
|
|
152
|
+
:param force: 是否强制删除正在运行的容器。
|
|
153
|
+
"""
|
|
154
|
+
self._container.remove(force=force)
|
|
155
|
+
print(f'[{self._connstr}] Removed container {self._container.name}')
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def existed(self) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
是否一个已存在的容器(即不是从镜像临时创建的)。
|
|
161
|
+
"""
|
|
162
|
+
return not self._image
|
|
163
|
+
|
|
164
|
+
@autopen()
|
|
165
|
+
def exec(
|
|
166
|
+
self,
|
|
167
|
+
cmd: str,
|
|
168
|
+
envs: Optional[Dict[str, str]] = None
|
|
169
|
+
) -> CommandResult:
|
|
170
|
+
"""
|
|
171
|
+
执行命令。
|
|
172
|
+
|
|
173
|
+
:param cmd: 被执行的命令。
|
|
174
|
+
:param envs: 环境变量。
|
|
175
|
+
:return: 命令输出。
|
|
176
|
+
|
|
177
|
+
:raises:
|
|
178
|
+
`CommandError` -- 命令返回码不为 0。
|
|
179
|
+
|
|
180
|
+
>>> exec('ls /home') # successful # doctest: +SKIP
|
|
181
|
+
>>> exec('ls /errpath') # CommandError # doctest: +SKIP
|
|
182
|
+
"""
|
|
183
|
+
envs = envs or {}
|
|
184
|
+
environment = self._envs.copy()
|
|
185
|
+
environment.update(envs)
|
|
186
|
+
print(f'[{self._connstr}:{self._cwd or "~"}] {cmd}')
|
|
187
|
+
cmdid = self._dockerclient.api.exec_create(
|
|
188
|
+
self._container.id,
|
|
189
|
+
# 使用 bash -c 的方式是为了避免 docker exec api 报 "cd not found" 的问题
|
|
190
|
+
'bash -c "' + (f'cd {self._cwd} && {cmd}' if self._cwd else cmd) + '"',
|
|
191
|
+
stdout=True,
|
|
192
|
+
stderr=True,
|
|
193
|
+
stdin=True,
|
|
194
|
+
tty=True,
|
|
195
|
+
environment=environment,
|
|
196
|
+
user=self._user
|
|
197
|
+
)['Id']
|
|
198
|
+
sock: SocketIO = self._dockerclient.api.exec_start(cmdid, tty=True, socket=True)
|
|
199
|
+
encoding = environment['LANG'].split('.')[-1]
|
|
200
|
+
output = ''
|
|
201
|
+
while True:
|
|
202
|
+
rlist, _, _ = select([sock], [], [], 0.1)
|
|
203
|
+
if sock in rlist:
|
|
204
|
+
data = sock.read(1024).decode(encoding=encoding, errors='ignore')
|
|
205
|
+
if data == '':
|
|
206
|
+
break
|
|
207
|
+
output += data
|
|
208
|
+
print(data, end='')
|
|
209
|
+
rc = self._dockerclient.api.exec_inspect(cmdid)['ExitCode']
|
|
210
|
+
if rc != 0:
|
|
211
|
+
raise CommandError(f'ExitCode {rc}: `{cmd}`')
|
|
212
|
+
return CommandResult(output, rc=rc, cmd=cmd)
|
|
213
|
+
|
|
214
|
+
@contextmanager
|
|
215
|
+
def dir(self, path: str | PurePosixPath) -> Generator[None, None, None]:
|
|
216
|
+
"""
|
|
217
|
+
切换工作目录。
|
|
218
|
+
|
|
219
|
+
>>> with dir('/my/workdir'): # doctest: +SKIP
|
|
220
|
+
... d = exec('pwd') # doctest: +SKIP
|
|
221
|
+
... # doctest: +SKIP
|
|
222
|
+
>>> d # doctest: +SKIP
|
|
223
|
+
'/my/workdir' # doctest: +SKIP
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
self._cwd = str(path)
|
|
227
|
+
yield
|
|
228
|
+
finally:
|
|
229
|
+
self._cwd = ''
|
|
230
|
+
|
|
231
|
+
@autopen()
|
|
232
|
+
def getfile(
|
|
233
|
+
self,
|
|
234
|
+
rfile: Union[str, PurePosixPath],
|
|
235
|
+
ldir: Union[str, Path]
|
|
236
|
+
) -> None:
|
|
237
|
+
"""
|
|
238
|
+
从容器下载文件 `rfile` 到本地目录 `ldir`。
|
|
239
|
+
|
|
240
|
+
:param rfile: 容器内文件。
|
|
241
|
+
:param ldir: 本地目录。
|
|
242
|
+
|
|
243
|
+
>>> getfile('/tmp/myfile', '/home') # /home/myfile
|
|
244
|
+
>>> getfile('/tmp/myfile', 'D:\\') # D:\\myfile
|
|
245
|
+
"""
|
|
246
|
+
rfile = PurePosixPath(rfile)
|
|
247
|
+
ldir = Path(ldir)
|
|
248
|
+
lfile = ldir.joinpath(rfile.name)
|
|
249
|
+
ltarfile = ldir.joinpath(f'{rfile.name}.tar')
|
|
250
|
+
print(f'[{self._connstr}] Get {lfile} <= {rfile}')
|
|
251
|
+
stream, _ = self._container.get_archive(str(rfile))
|
|
252
|
+
with open(ltarfile, 'wb') as f:
|
|
253
|
+
for chunk in stream:
|
|
254
|
+
f.write(chunk)
|
|
255
|
+
with tarfile.open(ltarfile) as tar:
|
|
256
|
+
tar.extractall(ldir)
|
|
257
|
+
os.remove(str(ltarfile))
|
|
258
|
+
|
|
259
|
+
@autopen()
|
|
260
|
+
def putfile(
|
|
261
|
+
self,
|
|
262
|
+
lfile: Union[str, Path],
|
|
263
|
+
rdir: Union[str, PurePosixPath]
|
|
264
|
+
) -> None:
|
|
265
|
+
"""
|
|
266
|
+
上传本地文件 `lfile` 到容器目录 `rdir`。
|
|
267
|
+
|
|
268
|
+
:param lfile: 本地文件。
|
|
269
|
+
:param rdir: 容器内目录。
|
|
270
|
+
|
|
271
|
+
>>> putfile('/home/myfile', '/tmp') # /tmp/myfile
|
|
272
|
+
>>> putfile('D:\\myfile', '/tmp') # /tmp/myfile
|
|
273
|
+
"""
|
|
274
|
+
lfile = Path(lfile)
|
|
275
|
+
rdir = PurePosixPath(rdir)
|
|
276
|
+
rfile = rdir.joinpath(lfile.name)
|
|
277
|
+
print(f'[{self._connstr}] Put {lfile} => {rfile}')
|
|
278
|
+
stream = io.BytesIO()
|
|
279
|
+
with tarfile.open(fileobj=stream, mode='w') as tar:
|
|
280
|
+
tarinfo = tar.gettarinfo(str(lfile), arcname=lfile.name)
|
|
281
|
+
tarinfo.uid = self._uid
|
|
282
|
+
with open(lfile, 'rb') as f:
|
|
283
|
+
tar.addfile(tarinfo, f)
|
|
284
|
+
stream.seek(0)
|
|
285
|
+
self._container.put_archive(str(rdir), stream)
|
|
286
|
+
|
|
287
|
+
@autopen()
|
|
288
|
+
def exists(self, path: Union[str, PurePosixPath]) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
检查远端路径是否存在。
|
|
291
|
+
"""
|
|
292
|
+
return self.exec(f'test -e {path} && echo true || echo flase') == 'true'
|
xflow/framework/env.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
环境信息管理。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ruamel import yaml
|
|
11
|
+
|
|
12
|
+
from xflow.framework.node import Node, NativeNode, ContainerNode
|
|
13
|
+
from xflow.framework.errors import NoSuchNodeError, NoSuchDockerError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Env(object):
|
|
17
|
+
"""
|
|
18
|
+
环境信息管理类。
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, envfile: str | Path):
|
|
21
|
+
"""
|
|
22
|
+
:param envfile: 环境信息文件。
|
|
23
|
+
"""
|
|
24
|
+
with open(envfile, encoding='utf8') as f:
|
|
25
|
+
self.__data = yaml.YAML(typ='safe').load(f)
|
|
26
|
+
self.__nodes: List[Node] = []
|
|
27
|
+
for nodeinfo in self.__data['nodes']:
|
|
28
|
+
if nodeinfo['docker']:
|
|
29
|
+
for dockerinfo in self.__data['dockers']:
|
|
30
|
+
if dockerinfo['name'] == nodeinfo['docker']:
|
|
31
|
+
self.__nodes.append(
|
|
32
|
+
ContainerNode(
|
|
33
|
+
name=nodeinfo['name'],
|
|
34
|
+
ip=dockerinfo['ip'],
|
|
35
|
+
port=dockerinfo['port'],
|
|
36
|
+
bwd=nodeinfo['workdir'],
|
|
37
|
+
user=nodeinfo['user'],
|
|
38
|
+
container=nodeinfo['container'],
|
|
39
|
+
image=nodeinfo['image'],
|
|
40
|
+
runargs=nodeinfo['runargs'],
|
|
41
|
+
cacert=dockerinfo['tls']['cacert'],
|
|
42
|
+
clientcert=dockerinfo['tls']['clientcert'],
|
|
43
|
+
clientkey=dockerinfo['tls']['clientkey'],
|
|
44
|
+
envs=nodeinfo['envs']
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
break
|
|
48
|
+
else:
|
|
49
|
+
raise NoSuchDockerError(f'name: {nodeinfo["docker"]}')
|
|
50
|
+
else:
|
|
51
|
+
self.__nodes.append(
|
|
52
|
+
NativeNode(
|
|
53
|
+
name=nodeinfo['name'],
|
|
54
|
+
ip=nodeinfo['ip'],
|
|
55
|
+
sshport=nodeinfo['sshport'],
|
|
56
|
+
user=nodeinfo['user'],
|
|
57
|
+
password=nodeinfo['password'],
|
|
58
|
+
bwd=nodeinfo['workdir'],
|
|
59
|
+
envs=nodeinfo['envs']
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def get_node(self, name: str) -> Node:
|
|
64
|
+
"""
|
|
65
|
+
获取节点。
|
|
66
|
+
|
|
67
|
+
:param name: 节点名。
|
|
68
|
+
"""
|
|
69
|
+
for node in self.__nodes:
|
|
70
|
+
if node.name == name:
|
|
71
|
+
return node
|
|
72
|
+
raise NoSuchNodeError(f'name: {name}')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
异常类。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SSHConnectError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
SSH 连接错误。
|
|
11
|
+
"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandError(Exception):
|
|
16
|
+
"""
|
|
17
|
+
命令执行错误。
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NoSuchNodeError(Exception):
|
|
23
|
+
"""
|
|
24
|
+
无此节点。
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoSuchDockerError(Exception):
|
|
30
|
+
"""
|
|
31
|
+
无此 Docker。
|
|
32
|
+
"""
|
|
33
|
+
pass
|
xflow/framework/main.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
入口脚本。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from typing import Type
|
|
14
|
+
from importlib import import_module
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from typed_settings import click_options
|
|
18
|
+
|
|
19
|
+
from xflow.framework.version import __version__
|
|
20
|
+
from xflow.framework.common import INIT_DIR
|
|
21
|
+
from xflow.framework.pipeline import Pipeline, TResult
|
|
22
|
+
from xflow.framework.env import Env
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RunGroup(click.Group):
|
|
26
|
+
"""
|
|
27
|
+
run 命令组。
|
|
28
|
+
"""
|
|
29
|
+
def import_pipeline(self, projdir: str, pplname: str) -> Type[Pipeline]:
|
|
30
|
+
"""
|
|
31
|
+
导入 Pipeline。
|
|
32
|
+
|
|
33
|
+
:param projdir: 项目目录。
|
|
34
|
+
:param pplname: pipeline 名称。
|
|
35
|
+
"""
|
|
36
|
+
projdir = str(Path(projdir).resolve())
|
|
37
|
+
if projdir not in sys.path:
|
|
38
|
+
sys.path.insert(0, projdir)
|
|
39
|
+
os.chdir(projdir)
|
|
40
|
+
pplmod = import_module(f'pipelines.{pplname}')
|
|
41
|
+
pplcls: Type[Pipeline] = getattr(pplmod, pplname)
|
|
42
|
+
return pplcls
|
|
43
|
+
|
|
44
|
+
def list_commands(self, ctx: click.Context):
|
|
45
|
+
"""
|
|
46
|
+
把 pipelines 目录下的 `.py` 文件名作为子命令(不递归)。
|
|
47
|
+
"""
|
|
48
|
+
cmds = []
|
|
49
|
+
ppldir = os.path.join(ctx.obj['projdir'], 'pipelines')
|
|
50
|
+
for f in os.listdir(ppldir):
|
|
51
|
+
if f.endswith('.py') and f not in ('__init__.py', 'template.py'):
|
|
52
|
+
cmds.append(f.replace('.py', ''))
|
|
53
|
+
return cmds
|
|
54
|
+
|
|
55
|
+
def get_command(self, ctx: click.Context, cmd_name: str):
|
|
56
|
+
"""
|
|
57
|
+
返回执行 pipeline 的函数。
|
|
58
|
+
"""
|
|
59
|
+
pplcls = self.import_pipeline(ctx.obj['projdir'], cmd_name)
|
|
60
|
+
@click.command(cmd_name)
|
|
61
|
+
@click_options(pplcls.Options, 'xflow')
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def command(ctx: click.Context, options: Pipeline.Options):
|
|
64
|
+
env = Env(Path(ctx.obj['projdir']).joinpath('env.yml'))
|
|
65
|
+
pplinst = pplcls(ctx.obj['projdir'],
|
|
66
|
+
env,
|
|
67
|
+
ctx.obj['nodename'],
|
|
68
|
+
options)
|
|
69
|
+
result: TResult = pplinst.run()
|
|
70
|
+
if result == 'FAILED':
|
|
71
|
+
exit(1)
|
|
72
|
+
return command
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@click.group()
|
|
76
|
+
@click.version_option(__version__)
|
|
77
|
+
@click.option('--projdir', '-p', required=True, envvar='XFLOW_PROJDIR', show_envvar=True)
|
|
78
|
+
@click.pass_context
|
|
79
|
+
def main(ctx: click.Context, projdir: str):
|
|
80
|
+
"""
|
|
81
|
+
xflow
|
|
82
|
+
"""
|
|
83
|
+
ctx.ensure_object(dict)
|
|
84
|
+
ctx.obj['projdir'] = projdir
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@main.command()
|
|
88
|
+
@click.pass_context
|
|
89
|
+
def init(ctx: click.Context):
|
|
90
|
+
"""
|
|
91
|
+
Initialize the project directory.
|
|
92
|
+
"""
|
|
93
|
+
projdir = ctx.obj['projdir']
|
|
94
|
+
if os.path.exists(projdir) and os.listdir(projdir):
|
|
95
|
+
print(f'Error: {projdir} exists and is not empty.')
|
|
96
|
+
exit(1)
|
|
97
|
+
shutil.copytree(INIT_DIR, projdir, dirs_exist_ok=True)
|
|
98
|
+
print(f'Initialized {projdir}')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@main.group(cls=RunGroup)
|
|
102
|
+
@click.option('--nodename', '-n', required=True)
|
|
103
|
+
@click.pass_context
|
|
104
|
+
def run(ctx: click.Context, nodename: str):
|
|
105
|
+
"""
|
|
106
|
+
Run a pipeline.
|
|
107
|
+
"""
|
|
108
|
+
ctx.obj['nodename'] = nodename
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
main()
|