QuLab 2.4.14__cp311-cp311-macosx_10_9_universal2.whl → 2.4.16__cp311-cp311-macosx_10_9_universal2.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.
- {QuLab-2.4.14.dist-info → QuLab-2.4.16.dist-info}/METADATA +1 -1
- {QuLab-2.4.14.dist-info → QuLab-2.4.16.dist-info}/RECORD +13 -11
- qulab/__main__.py +1 -29
- qulab/cli/__init__.py +29 -0
- qulab/cli/config.py +88 -0
- qulab/executor/cli.py +178 -0
- qulab/executor/load.py +59 -21
- qulab/fun.cpython-311-darwin.so +0 -0
- qulab/version.py +1 -1
- qulab/executor/__main__.py +0 -200
- {QuLab-2.4.14.dist-info → QuLab-2.4.16.dist-info}/LICENSE +0 -0
- {QuLab-2.4.14.dist-info → QuLab-2.4.16.dist-info}/WHEEL +0 -0
- {QuLab-2.4.14.dist-info → QuLab-2.4.16.dist-info}/entry_points.txt +0 -0
- {QuLab-2.4.14.dist-info → QuLab-2.4.16.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
qulab/__init__.py,sha256=P-Mx2p4TVmL91SoxoeXcj8Qm0x4xUf5Q_FLk0Yc_gIQ,138
|
|
2
|
-
qulab/__main__.py,sha256=
|
|
2
|
+
qulab/__main__.py,sha256=JgErYkiskih8Y6oRwowALtR-rwQhAAdqOYWjQraRIPI,59
|
|
3
3
|
qulab/dicttree.py,sha256=tRRMpGZYVOLw0TEByE3_2Ss8FdOmzuGL9e1DWbs8qoY,13684
|
|
4
|
-
qulab/fun.cpython-311-darwin.so,sha256=
|
|
5
|
-
qulab/version.py,sha256=
|
|
4
|
+
qulab/fun.cpython-311-darwin.so,sha256=4UizSKt5rDt7vdZVD0pkpw5COMzc0CVh4OpeSim3jFU,126848
|
|
5
|
+
qulab/version.py,sha256=rVOnxEJVvl0ZWvCblBzVBGcp0VALbV_koS5W-UB9zfk,22
|
|
6
|
+
qulab/cli/__init__.py,sha256=tgDIkkeIoasQXAifJZ6NU8jDgpNgb2a-B0C4nF0evrE,559
|
|
7
|
+
qulab/cli/config.py,sha256=QksTYD3-RBYBG8xnKLAh7EXrNOwxr128XXr30ZTFW6A,2983
|
|
6
8
|
qulab/executor/__init__.py,sha256=LosPzOMaljSZY1thy_Fxtbrgq7uubJszMABEB7oM7tU,101
|
|
7
|
-
qulab/executor/
|
|
8
|
-
qulab/executor/load.py,sha256=
|
|
9
|
+
qulab/executor/cli.py,sha256=LRSb5-J8Tn7dj-fxwCp3OP_6YUgPRoY-f8O18sbWG8I,5164
|
|
10
|
+
qulab/executor/load.py,sha256=yTLoWOtTvmCTuYFZ6qFsjBM_CvsrKgmUkBSHHz3223A,10899
|
|
9
11
|
qulab/executor/schedule.py,sha256=SfMZys8WdmIvfW5fPM42g-F6XwewTdpC0-puhf5TA54,11721
|
|
10
12
|
qulab/executor/storage.py,sha256=M66Q5_Uc5MMfc_QAuuaaexwAz7wxBPMkeleB5nRpQmI,4621
|
|
11
13
|
qulab/executor/transform.py,sha256=inaOn6eqCs22ZZ0xAQl8s8YCoEACaXSwFNNu7jqdwAk,2148
|
|
@@ -89,9 +91,9 @@ qulab/visualization/plot_seq.py,sha256=UWTS6p9nfX_7B8ehcYo6UnSTUCjkBsNU9jiOeW2ca
|
|
|
89
91
|
qulab/visualization/qdat.py,sha256=ZeevBYWkzbww4xZnsjHhw7wRorJCBzbG0iEu-XQB4EA,5735
|
|
90
92
|
qulab/visualization/rot3d.py,sha256=lMrEJlRLwYe6NMBlGkKYpp_V9CTipOAuDy6QW_cQK00,734
|
|
91
93
|
qulab/visualization/widgets.py,sha256=6KkiTyQ8J-ei70LbPQZAK35wjktY47w2IveOa682ftA,3180
|
|
92
|
-
QuLab-2.4.
|
|
93
|
-
QuLab-2.4.
|
|
94
|
-
QuLab-2.4.
|
|
95
|
-
QuLab-2.4.
|
|
96
|
-
QuLab-2.4.
|
|
97
|
-
QuLab-2.4.
|
|
94
|
+
QuLab-2.4.16.dist-info/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
|
|
95
|
+
QuLab-2.4.16.dist-info/METADATA,sha256=6JcU_aAzDy5e3eGklWSR_xUELNWTZukNp65w5mch6ws,3699
|
|
96
|
+
QuLab-2.4.16.dist-info/WHEEL,sha256=K10eKSN6_vzvMOgXxWbVOQNR7Orfl6gBTCpCI8bcYx4,114
|
|
97
|
+
QuLab-2.4.16.dist-info/entry_points.txt,sha256=b0v1GXOwmxY-nCCsPN_rHZZvY9CtTbWqrGj8u1m8yHo,45
|
|
98
|
+
QuLab-2.4.16.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
|
|
99
|
+
QuLab-2.4.16.dist-info/RECORD,,
|
qulab/__main__.py
CHANGED
|
@@ -1,32 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
from .executor.__main__ import create, get, maintain, run, set
|
|
4
|
-
from .monitor.__main__ import main as monitor
|
|
5
|
-
from .scan.server import server
|
|
6
|
-
from .sys.net.cli import dht
|
|
7
|
-
from .visualization.__main__ import plot
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@click.group()
|
|
11
|
-
def cli():
|
|
12
|
-
pass
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@cli.command()
|
|
16
|
-
def hello():
|
|
17
|
-
"""Print hello world."""
|
|
18
|
-
click.echo('hello, world')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
cli.add_command(monitor)
|
|
22
|
-
cli.add_command(plot)
|
|
23
|
-
cli.add_command(dht)
|
|
24
|
-
cli.add_command(server)
|
|
25
|
-
cli.add_command(maintain)
|
|
26
|
-
cli.add_command(run)
|
|
27
|
-
cli.add_command(create)
|
|
28
|
-
cli.add_command(set)
|
|
29
|
-
cli.add_command(get)
|
|
1
|
+
from .cli import cli
|
|
30
2
|
|
|
31
3
|
if __name__ == '__main__':
|
|
32
4
|
cli()
|
qulab/cli/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from ..executor.cli import create, get, maintain, run, set
|
|
4
|
+
from ..monitor.__main__ import main as monitor
|
|
5
|
+
from ..scan.server import server
|
|
6
|
+
from ..sys.net.cli import dht
|
|
7
|
+
from ..visualization.__main__ import plot
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def cli():
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.command()
|
|
16
|
+
def hello():
|
|
17
|
+
"""Print hello world."""
|
|
18
|
+
click.echo('hello, world')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli.add_command(monitor)
|
|
22
|
+
cli.add_command(plot)
|
|
23
|
+
cli.add_command(dht)
|
|
24
|
+
cli.add_command(server)
|
|
25
|
+
cli.add_command(maintain)
|
|
26
|
+
cli.add_command(run)
|
|
27
|
+
cli.add_command(create)
|
|
28
|
+
cli.add_command(set)
|
|
29
|
+
cli.add_command(get)
|
qulab/cli/config.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import functools
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
CONFIG_PATH = os.path.expanduser("~/.qulab.ini")
|
|
11
|
+
ENV_PREFIX = "QULAB_"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_config_value(option_name, type_cast=str, command_name=None):
|
|
15
|
+
"""支持命令专属配置的优先级获取"""
|
|
16
|
+
# 构造环境变量名
|
|
17
|
+
if command_name:
|
|
18
|
+
env_var = f"{ENV_PREFIX}{command_name.upper()}_{option_name.upper()}"
|
|
19
|
+
config_section = command_name
|
|
20
|
+
else:
|
|
21
|
+
env_var = f"{ENV_PREFIX}{option_name.upper()}"
|
|
22
|
+
config_section = "common"
|
|
23
|
+
|
|
24
|
+
# 1. 检查环境变量
|
|
25
|
+
if env_value := os.getenv(env_var):
|
|
26
|
+
if type_cast is bool:
|
|
27
|
+
return env_value.lower() in ("true", "1", "yes")
|
|
28
|
+
if "path" in option_name:
|
|
29
|
+
return os.path.expanduser(env_value)
|
|
30
|
+
return type_cast(env_value)
|
|
31
|
+
|
|
32
|
+
# 2. 检查配置文件
|
|
33
|
+
config = configparser.ConfigParser()
|
|
34
|
+
# 先加载默认配置防止段不存在
|
|
35
|
+
config.read_dict({config_section: {}})
|
|
36
|
+
if Path(CONFIG_PATH).exists():
|
|
37
|
+
config.read(CONFIG_PATH)
|
|
38
|
+
|
|
39
|
+
# 从对应配置段读取
|
|
40
|
+
if config.has_section(config_section):
|
|
41
|
+
if config_value := config.get(config_section,
|
|
42
|
+
option_name,
|
|
43
|
+
fallback=None):
|
|
44
|
+
if type_cast is bool:
|
|
45
|
+
return config_value.lower() in ("true", "1", "yes")
|
|
46
|
+
if "path" in option_name:
|
|
47
|
+
return os.path.expanduser(config_value)
|
|
48
|
+
return type_cast(config_value)
|
|
49
|
+
|
|
50
|
+
return None # 交给 Click 处理默认值
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_config_value(option_name, type_cast=str, command_name=None):
|
|
54
|
+
value = _get_config_value(option_name, type_cast, command_name)
|
|
55
|
+
if value is None and command_name is not None:
|
|
56
|
+
return _get_config_value(option_name, type_cast)
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def log_options(func):
|
|
61
|
+
"""通用配置装饰器(所有命令共用)"""
|
|
62
|
+
|
|
63
|
+
@click.option("--debug",
|
|
64
|
+
is_flag=True,
|
|
65
|
+
default=lambda: get_config_value("debug", bool) or False,
|
|
66
|
+
help=f"Enable debug mode")
|
|
67
|
+
@click.option("--log",
|
|
68
|
+
type=click.Path(),
|
|
69
|
+
default=lambda: get_config_value("log"),
|
|
70
|
+
help=f"Log file path")
|
|
71
|
+
@functools.wraps(func)
|
|
72
|
+
def wrapper(*args, log=None, debug=False, **kwargs):
|
|
73
|
+
if log is None and not debug:
|
|
74
|
+
logger.remove()
|
|
75
|
+
logger.add(sys.stderr, level='INFO')
|
|
76
|
+
elif log is None and debug:
|
|
77
|
+
logger.remove()
|
|
78
|
+
logger.add(sys.stderr, level='DEBUG')
|
|
79
|
+
elif log is not None and not debug:
|
|
80
|
+
logger.configure(handlers=[dict(sink=log, level='INFO')])
|
|
81
|
+
elif log is not None and debug:
|
|
82
|
+
logger.configure(handlers=[
|
|
83
|
+
dict(sink=log, level='DEBUG'),
|
|
84
|
+
dict(sink=sys.stderr, level='DEBUG')
|
|
85
|
+
])
|
|
86
|
+
return func(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
return wrapper
|
qulab/executor/cli.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from ..cli.config import ENV_PREFIX, get_config_value, log_options
|
|
11
|
+
from .load import find_unreferenced_workflows, get_entries, load_workflow
|
|
12
|
+
from .schedule import maintain as maintain_workflow
|
|
13
|
+
from .schedule import run as run_workflow
|
|
14
|
+
from .transform import set_config_api
|
|
15
|
+
from .utils import workflow_template
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def command_option(command_name):
|
|
19
|
+
"""命令专属配置装饰器工厂"""
|
|
20
|
+
|
|
21
|
+
def decorator(func):
|
|
22
|
+
|
|
23
|
+
@click.option(
|
|
24
|
+
'--code',
|
|
25
|
+
'-c',
|
|
26
|
+
default=lambda: get_config_value("code", str, command_name),
|
|
27
|
+
help='The path of the code.')
|
|
28
|
+
@click.option(
|
|
29
|
+
'--data',
|
|
30
|
+
'-d',
|
|
31
|
+
default=lambda: get_config_value("data", str, command_name),
|
|
32
|
+
help='The path of the data.')
|
|
33
|
+
@click.option(
|
|
34
|
+
'--api',
|
|
35
|
+
'-a',
|
|
36
|
+
default=lambda: get_config_value("api", str, command_name),
|
|
37
|
+
help='The modlule name of the api.')
|
|
38
|
+
@functools.wraps(func)
|
|
39
|
+
def wrapper(*args, **kwargs):
|
|
40
|
+
return func(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
return wrapper
|
|
43
|
+
|
|
44
|
+
return decorator
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.command()
|
|
48
|
+
@click.argument('workflow')
|
|
49
|
+
@click.option('--code',
|
|
50
|
+
'-c',
|
|
51
|
+
default=lambda: get_config_value("code", str, 'create'),
|
|
52
|
+
help='The path of the code.')
|
|
53
|
+
def create(workflow, code):
|
|
54
|
+
"""
|
|
55
|
+
Create a new workflow file.
|
|
56
|
+
"""
|
|
57
|
+
if code is None:
|
|
58
|
+
code = Path.cwd()
|
|
59
|
+
|
|
60
|
+
fname = Path(code) / f'{workflow}'
|
|
61
|
+
fname = Path(os.path.expanduser(fname))
|
|
62
|
+
if fname.exists():
|
|
63
|
+
click.echo(f'{workflow} already exists')
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
fname.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
deps = find_unreferenced_workflows(code)
|
|
68
|
+
|
|
69
|
+
with open(fname, 'w') as f:
|
|
70
|
+
f.write(workflow_template(list(deps)))
|
|
71
|
+
click.echo(f'{workflow} created')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@click.command()
|
|
75
|
+
@click.argument('key')
|
|
76
|
+
@click.argument('value', type=str)
|
|
77
|
+
@click.option('--api',
|
|
78
|
+
'-a',
|
|
79
|
+
default=lambda: get_config_value("api", str, 'set'),
|
|
80
|
+
help='The modlule name of the api.')
|
|
81
|
+
def set(key, value, api):
|
|
82
|
+
"""
|
|
83
|
+
Set a config.
|
|
84
|
+
"""
|
|
85
|
+
from . import transform
|
|
86
|
+
if api is not None:
|
|
87
|
+
api = importlib.import_module(api)
|
|
88
|
+
set_config_api(api.query_config, api.update_config)
|
|
89
|
+
try:
|
|
90
|
+
value = eval(value)
|
|
91
|
+
except:
|
|
92
|
+
pass
|
|
93
|
+
transform.update_config({key: value})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@click.command()
|
|
97
|
+
@click.argument('key')
|
|
98
|
+
@click.option('--api',
|
|
99
|
+
'-a',
|
|
100
|
+
default=lambda: get_config_value("api", str, 'get'),
|
|
101
|
+
help='The modlule name of the api.')
|
|
102
|
+
def get(key, api):
|
|
103
|
+
"""
|
|
104
|
+
Get a config.
|
|
105
|
+
"""
|
|
106
|
+
from . import transform
|
|
107
|
+
if api is not None:
|
|
108
|
+
api = importlib.import_module(api)
|
|
109
|
+
set_config_api(api.query_config, api.update_config)
|
|
110
|
+
click.echo(transform.query_config(key))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@click.command()
|
|
114
|
+
@click.argument('workflow')
|
|
115
|
+
@click.option('--plot', '-p', is_flag=True, help='Plot the result.')
|
|
116
|
+
@click.option('--no-dependents',
|
|
117
|
+
'-n',
|
|
118
|
+
is_flag=True,
|
|
119
|
+
help='Do not run dependents.')
|
|
120
|
+
@log_options
|
|
121
|
+
@command_option('run')
|
|
122
|
+
def run(workflow, code, data, api, plot, no_dependents):
|
|
123
|
+
"""
|
|
124
|
+
Run a workflow.
|
|
125
|
+
"""
|
|
126
|
+
if api is not None:
|
|
127
|
+
api = importlib.import_module(api)
|
|
128
|
+
set_config_api(api.query_config, api.update_config)
|
|
129
|
+
if code is None:
|
|
130
|
+
code = Path.cwd()
|
|
131
|
+
if data is None:
|
|
132
|
+
data = Path(code) / 'logs'
|
|
133
|
+
|
|
134
|
+
code = Path(os.path.expanduser(code))
|
|
135
|
+
data = Path(os.path.expanduser(data))
|
|
136
|
+
|
|
137
|
+
wf = load_workflow(workflow, code)
|
|
138
|
+
|
|
139
|
+
if no_dependents:
|
|
140
|
+
if hasattr(wf, 'entries'):
|
|
141
|
+
for entry in get_entries(wf, code):
|
|
142
|
+
run_workflow(entry, code, data, plot=plot)
|
|
143
|
+
else:
|
|
144
|
+
run_workflow(wf, code, data, plot=plot)
|
|
145
|
+
else:
|
|
146
|
+
if hasattr(wf, 'entries'):
|
|
147
|
+
for entry in get_entries(wf, code):
|
|
148
|
+
maintain_workflow(entry, code, data, run=True, plot=plot)
|
|
149
|
+
else:
|
|
150
|
+
maintain_workflow(wf, code, data, run=True, plot=plot)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@click.command()
|
|
154
|
+
@click.argument('workflow')
|
|
155
|
+
@click.option('--plot', '-p', is_flag=True, help='Plot the result.')
|
|
156
|
+
@log_options
|
|
157
|
+
@command_option('maintain')
|
|
158
|
+
def maintain(workflow, code, data, api, plot):
|
|
159
|
+
"""
|
|
160
|
+
Maintain a workflow.
|
|
161
|
+
"""
|
|
162
|
+
if api is not None:
|
|
163
|
+
api = importlib.import_module(api)
|
|
164
|
+
set_config_api(api.query_config, api.update_config)
|
|
165
|
+
if code is None:
|
|
166
|
+
code = Path.cwd()
|
|
167
|
+
if data is None:
|
|
168
|
+
data = Path(code) / 'logs'
|
|
169
|
+
|
|
170
|
+
code = Path(os.path.expanduser(code))
|
|
171
|
+
data = Path(os.path.expanduser(data))
|
|
172
|
+
|
|
173
|
+
wf = load_workflow(workflow, code)
|
|
174
|
+
if hasattr(wf, 'entries'):
|
|
175
|
+
for entry in get_entries(wf, code):
|
|
176
|
+
maintain_workflow(entry, code, data, run=False, plot=plot)
|
|
177
|
+
else:
|
|
178
|
+
maintain_workflow(wf, code, data, run=False, plot=plot)
|
qulab/executor/load.py
CHANGED
|
@@ -15,6 +15,7 @@ from .storage import Result
|
|
|
15
15
|
|
|
16
16
|
class SetConfigWorkflow():
|
|
17
17
|
__timeout__ = None
|
|
18
|
+
__mtime__ = 0
|
|
18
19
|
|
|
19
20
|
def __init__(self, key):
|
|
20
21
|
self.key = key
|
|
@@ -119,6 +120,17 @@ def is_workflow(module: ModuleType) -> bool:
|
|
|
119
120
|
return False
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
def is_template(path: str | Path) -> bool:
|
|
124
|
+
path = Path(path)
|
|
125
|
+
if path.name == 'template.py':
|
|
126
|
+
return True
|
|
127
|
+
if path.name.endswith('_template.py'):
|
|
128
|
+
return True
|
|
129
|
+
if 'templates' in path.parts:
|
|
130
|
+
return True
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
122
134
|
def find_unreferenced_workflows(path: str) -> list[str]:
|
|
123
135
|
root = Path(path).resolve()
|
|
124
136
|
workflows = []
|
|
@@ -128,8 +140,7 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
128
140
|
for file_path in root.rglob("*.py"):
|
|
129
141
|
if file_path.name == "__init__.py":
|
|
130
142
|
continue
|
|
131
|
-
if file_path
|
|
132
|
-
"_template.py"):
|
|
143
|
+
if is_template(file_path):
|
|
133
144
|
continue
|
|
134
145
|
try:
|
|
135
146
|
rel_path = file_path.relative_to(root)
|
|
@@ -188,14 +199,18 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
188
199
|
def load_workflow_from_file(file_name: str,
|
|
189
200
|
base_path: str | Path,
|
|
190
201
|
package='workflows') -> WorkflowType:
|
|
191
|
-
if file_name.startswith('cfg:'):
|
|
192
|
-
return SetConfigWorkflow(file_name[4:])
|
|
193
202
|
base_path = Path(base_path)
|
|
194
203
|
path = Path(file_name)
|
|
204
|
+
if not (base_path / path).exists():
|
|
205
|
+
raise FileNotFoundError(f"File not found: {base_path / path}")
|
|
195
206
|
module_name = f"{package}.{'.'.join([*path.parts[:-1], path.stem])}"
|
|
196
207
|
spec = spec_from_file_location(module_name, base_path / path)
|
|
197
208
|
module = module_from_spec(spec)
|
|
198
209
|
spec.loader.exec_module(module)
|
|
210
|
+
module.__mtime__ = (base_path / path).stat().st_mtime
|
|
211
|
+
|
|
212
|
+
if hasattr(module, 'entries'):
|
|
213
|
+
return module
|
|
199
214
|
|
|
200
215
|
if not hasattr(module, '__timeout__'):
|
|
201
216
|
module.__timeout__ = None
|
|
@@ -209,17 +224,20 @@ def load_workflow_from_file(file_name: str,
|
|
|
209
224
|
return module
|
|
210
225
|
|
|
211
226
|
|
|
212
|
-
def load_workflow_from_template(
|
|
227
|
+
def load_workflow_from_template(template_path: str,
|
|
213
228
|
mappping: dict[str, str],
|
|
214
229
|
base_path: str | Path,
|
|
215
|
-
|
|
216
|
-
package='workflows'
|
|
230
|
+
target_path: str | None = None,
|
|
231
|
+
package='workflows',
|
|
232
|
+
mtime: float = 0) -> WorkflowType:
|
|
217
233
|
base_path = Path(base_path)
|
|
218
|
-
path = Path(
|
|
234
|
+
path = Path(template_path)
|
|
219
235
|
|
|
220
236
|
with open(base_path / path) as f:
|
|
221
237
|
content = f.read()
|
|
222
238
|
|
|
239
|
+
mtime = max(Path(template_path).stat().st_mtime, mtime)
|
|
240
|
+
|
|
223
241
|
def replace(text):
|
|
224
242
|
"""
|
|
225
243
|
将给定文本中的所有 _D_("var") 替换为 ${var}。
|
|
@@ -239,41 +257,51 @@ def load_workflow_from_template(file_name: str,
|
|
|
239
257
|
content = template.substitute(mappping)
|
|
240
258
|
|
|
241
259
|
hash_str = hashlib.md5(pickle.dumps(mappping)).hexdigest()[:8]
|
|
242
|
-
if
|
|
260
|
+
if target_path is None:
|
|
243
261
|
if path.stem == 'template':
|
|
244
262
|
path = path.parent / f'tmp{hash_str}.py'
|
|
245
|
-
|
|
263
|
+
elif path.stem.endswith('_template'):
|
|
246
264
|
path = path.parent / path.stem.replace('_template',
|
|
247
265
|
f'_tmp{hash_str}.py')
|
|
248
|
-
else:
|
|
249
|
-
if path.stem == 'template':
|
|
250
|
-
path = path.parent / f'{subtitle}.py'
|
|
251
266
|
else:
|
|
252
|
-
path = path.parent / path.stem.
|
|
253
|
-
|
|
267
|
+
path = path.parent / f'{path.stem}_tmp{hash_str}.py'
|
|
268
|
+
else:
|
|
269
|
+
path = target_path
|
|
254
270
|
|
|
255
271
|
file = base_path / path
|
|
256
272
|
if not file.exists():
|
|
273
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
257
274
|
with open(file, 'w') as f:
|
|
258
275
|
f.write(content)
|
|
276
|
+
else:
|
|
277
|
+
if file.stat().st_mtime < mtime:
|
|
278
|
+
with open(file, 'w') as f:
|
|
279
|
+
f.write(content)
|
|
280
|
+
else:
|
|
281
|
+
if file.read_text() != content:
|
|
282
|
+
logger.warning(
|
|
283
|
+
f"`{file}` already exists and is different from the new one generated from template `{template_path}`"
|
|
284
|
+
)
|
|
259
285
|
|
|
260
286
|
module = load_workflow_from_file(str(path), base_path, package)
|
|
287
|
+
module.__mtime__ = max(mtime, module.__mtime__)
|
|
261
288
|
|
|
262
289
|
return module
|
|
263
290
|
|
|
264
291
|
|
|
265
292
|
def load_workflow(workflow: str | tuple[str, dict],
|
|
266
293
|
base_path: str | Path,
|
|
267
|
-
package='workflows'
|
|
294
|
+
package='workflows',
|
|
295
|
+
mtime: float = 0) -> WorkflowType:
|
|
268
296
|
if isinstance(workflow, tuple):
|
|
269
297
|
if len(workflow) == 2:
|
|
270
298
|
file_name, mapping = workflow
|
|
271
299
|
w = load_workflow_from_template(file_name, mapping, base_path,
|
|
272
|
-
None, package)
|
|
300
|
+
None, package, mtime)
|
|
273
301
|
elif len(workflow) == 3:
|
|
274
|
-
|
|
275
|
-
w = load_workflow_from_template(
|
|
276
|
-
|
|
302
|
+
template_path, target_path, mapping = workflow
|
|
303
|
+
w = load_workflow_from_template(template_path, mapping, base_path,
|
|
304
|
+
target_path, package, mtime)
|
|
277
305
|
else:
|
|
278
306
|
raise ValueError(f"Invalid workflow: {workflow}")
|
|
279
307
|
w.__workflow_id__ = str(Path(w.__file__).relative_to(base_path))
|
|
@@ -293,4 +321,14 @@ def load_workflow(workflow: str | tuple[str, dict],
|
|
|
293
321
|
|
|
294
322
|
def get_dependents(workflow: WorkflowType,
|
|
295
323
|
code_path: str | Path) -> list[WorkflowType]:
|
|
296
|
-
return [
|
|
324
|
+
return [
|
|
325
|
+
load_workflow(n, code_path, mtime=workflow.__mtime__)
|
|
326
|
+
for n in workflow.depends()[0]
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def get_entries(workflow: WorkflowType, code_path: str | Path) -> WorkflowType:
|
|
331
|
+
return [
|
|
332
|
+
load_workflow(n, code_path, mtime=workflow.__mtime__)
|
|
333
|
+
for n in workflow.entries()
|
|
334
|
+
]
|
qulab/fun.cpython-311-darwin.so
CHANGED
|
Binary file
|
qulab/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.4.
|
|
1
|
+
__version__ = "2.4.16"
|
qulab/executor/__main__.py
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import importlib
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import click
|
|
8
|
-
from loguru import logger
|
|
9
|
-
|
|
10
|
-
from .load import find_unreferenced_workflows, load_workflow
|
|
11
|
-
from .schedule import maintain as maintain_workflow
|
|
12
|
-
from .schedule import run as run_workflow
|
|
13
|
-
from .transform import set_config_api
|
|
14
|
-
from .utils import workflow_template
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def load_config():
|
|
18
|
-
import yaml
|
|
19
|
-
|
|
20
|
-
config_paths = [
|
|
21
|
-
Path.home() / ".myapp/config.yaml", # 用户主目录
|
|
22
|
-
Path("config.yaml") # 当前目录
|
|
23
|
-
]
|
|
24
|
-
for path in config_paths:
|
|
25
|
-
if path.exists():
|
|
26
|
-
with open(path) as f:
|
|
27
|
-
return yaml.safe_load(f)
|
|
28
|
-
return {"defaults": {"log": "default.log", "debug": False}}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def get_config_value(option_name):
|
|
32
|
-
# 1. 尝试从环境变量读取
|
|
33
|
-
env_value = os.environ.get(f"MYAPP_{option_name.upper()}")
|
|
34
|
-
if env_value:
|
|
35
|
-
return env_value
|
|
36
|
-
|
|
37
|
-
# 2. 尝试从配置文件读取
|
|
38
|
-
config = load_config()
|
|
39
|
-
return config["defaults"].get(option_name)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def log_options(func):
|
|
43
|
-
|
|
44
|
-
@click.option("--debug", is_flag=True, help="Enable debug mode.")
|
|
45
|
-
@click.option("--log", type=str, help="Log file path.")
|
|
46
|
-
@functools.wraps(func) # 保持函数元信息
|
|
47
|
-
def wrapper(*args, log=None, debug=False, **kwargs):
|
|
48
|
-
if log is None and not debug:
|
|
49
|
-
logger.remove()
|
|
50
|
-
logger.add(sys.stderr, level='INFO')
|
|
51
|
-
elif log is None and debug:
|
|
52
|
-
logger.remove()
|
|
53
|
-
logger.add(sys.stderr, level='DEBUG')
|
|
54
|
-
elif log is not None and not debug:
|
|
55
|
-
logger.configure(handlers=[dict(sink=log, level='INFO')])
|
|
56
|
-
elif log is not None and debug:
|
|
57
|
-
logger.configure(handlers=[
|
|
58
|
-
dict(sink=log, level='DEBUG'),
|
|
59
|
-
dict(sink=sys.stderr, level='DEBUG')
|
|
60
|
-
])
|
|
61
|
-
return func(*args, **kwargs)
|
|
62
|
-
|
|
63
|
-
return wrapper
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@click.group()
|
|
67
|
-
def cli():
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@click.command()
|
|
72
|
-
@click.argument('workflow')
|
|
73
|
-
@click.option('--code', '-c', default=None, help='The path of the code.')
|
|
74
|
-
def create(workflow, code):
|
|
75
|
-
"""
|
|
76
|
-
Create a new workflow file.
|
|
77
|
-
"""
|
|
78
|
-
if code is None:
|
|
79
|
-
code = Path.cwd()
|
|
80
|
-
|
|
81
|
-
fname = Path(code) / f'{workflow}'
|
|
82
|
-
fname = Path(os.path.expanduser(fname))
|
|
83
|
-
if fname.exists():
|
|
84
|
-
click.echo(f'{workflow} already exists')
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
fname.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
-
deps = find_unreferenced_workflows(code)
|
|
89
|
-
|
|
90
|
-
with open(fname, 'w') as f:
|
|
91
|
-
f.write(workflow_template(list(deps)))
|
|
92
|
-
click.echo(f'{workflow} created')
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@click.command()
|
|
96
|
-
@click.argument('key')
|
|
97
|
-
@click.argument('value', type=str)
|
|
98
|
-
@click.option('--api', '-a', default=None, help='The modlule name of the api.')
|
|
99
|
-
def set(key, value, api):
|
|
100
|
-
"""
|
|
101
|
-
Set a config.
|
|
102
|
-
"""
|
|
103
|
-
from . import transform
|
|
104
|
-
if api is not None:
|
|
105
|
-
api = importlib.import_module(api)
|
|
106
|
-
set_config_api(api.query_config, api.update_config)
|
|
107
|
-
try:
|
|
108
|
-
value = eval(value)
|
|
109
|
-
except:
|
|
110
|
-
pass
|
|
111
|
-
transform.update_config({key: value})
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@click.command()
|
|
115
|
-
@click.argument('key')
|
|
116
|
-
@click.option('--api', '-a', default=None, help='The modlule name of the api.')
|
|
117
|
-
def get(key, api):
|
|
118
|
-
"""
|
|
119
|
-
Get a config.
|
|
120
|
-
"""
|
|
121
|
-
from . import transform
|
|
122
|
-
if api is not None:
|
|
123
|
-
api = importlib.import_module(api)
|
|
124
|
-
set_config_api(api.query_config, api.update_config)
|
|
125
|
-
click.echo(transform.query_config(key))
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
@click.command()
|
|
129
|
-
@click.argument('workflow')
|
|
130
|
-
@click.option('--code', '-c', default=None, help='The path of the code.')
|
|
131
|
-
@click.option('--data', '-d', default=None, help='The path of the data.')
|
|
132
|
-
@click.option('--api', '-a', default=None, help='The modlule name of the api.')
|
|
133
|
-
@click.option('--plot', '-p', is_flag=True, help='Plot the result.')
|
|
134
|
-
@click.option('--no-dependents',
|
|
135
|
-
'-n',
|
|
136
|
-
is_flag=True,
|
|
137
|
-
help='Do not run dependents.')
|
|
138
|
-
@log_options
|
|
139
|
-
def run(workflow, code, data, api, plot, no_dependents):
|
|
140
|
-
"""
|
|
141
|
-
Run a workflow.
|
|
142
|
-
"""
|
|
143
|
-
if api is not None:
|
|
144
|
-
api = importlib.import_module(api)
|
|
145
|
-
set_config_api(api.query_config, api.update_config)
|
|
146
|
-
if code is None:
|
|
147
|
-
code = Path.cwd()
|
|
148
|
-
if data is None:
|
|
149
|
-
data = Path(code) / 'logs'
|
|
150
|
-
|
|
151
|
-
code = Path(os.path.expanduser(code))
|
|
152
|
-
data = Path(os.path.expanduser(data))
|
|
153
|
-
|
|
154
|
-
if no_dependents:
|
|
155
|
-
run_workflow(load_workflow(workflow, code), code, data, plot=plot)
|
|
156
|
-
else:
|
|
157
|
-
maintain_workflow(load_workflow(workflow, code),
|
|
158
|
-
code,
|
|
159
|
-
data,
|
|
160
|
-
run=True,
|
|
161
|
-
plot=plot)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
@click.command()
|
|
165
|
-
@click.argument('workflow')
|
|
166
|
-
@click.option('--code', '-c', default=None, help='The path of the code.')
|
|
167
|
-
@click.option('--data', '-d', default=None, help='The path of the data.')
|
|
168
|
-
@click.option('--api', '-a', default=None, help='The modlule name of the api.')
|
|
169
|
-
@click.option('--plot', '-p', is_flag=True, help='Plot the result.')
|
|
170
|
-
@log_options
|
|
171
|
-
def maintain(workflow, code, data, api, plot):
|
|
172
|
-
"""
|
|
173
|
-
Maintain a workflow.
|
|
174
|
-
"""
|
|
175
|
-
if api is not None:
|
|
176
|
-
api = importlib.import_module(api)
|
|
177
|
-
set_config_api(api.query_config, api.update_config)
|
|
178
|
-
if code is None:
|
|
179
|
-
code = Path.cwd()
|
|
180
|
-
if data is None:
|
|
181
|
-
data = Path(code) / 'logs'
|
|
182
|
-
|
|
183
|
-
code = Path(os.path.expanduser(code))
|
|
184
|
-
data = Path(os.path.expanduser(data))
|
|
185
|
-
|
|
186
|
-
maintain_workflow(load_workflow(workflow, code),
|
|
187
|
-
code,
|
|
188
|
-
data,
|
|
189
|
-
run=False,
|
|
190
|
-
plot=plot)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
cli.add_command(maintain)
|
|
194
|
-
cli.add_command(run)
|
|
195
|
-
cli.add_command(create)
|
|
196
|
-
cli.add_command(set)
|
|
197
|
-
cli.add_command(get)
|
|
198
|
-
|
|
199
|
-
if __name__ == '__main__':
|
|
200
|
-
cli()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|