QuLab 2.4.13__cp310-cp310-macosx_10_9_universal2.whl → 2.4.15__cp310-cp310-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.13.dist-info → QuLab-2.4.15.dist-info}/METADATA +1 -1
- {QuLab-2.4.13.dist-info → QuLab-2.4.15.dist-info}/RECORD +15 -13
- 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 +71 -28
- qulab/executor/schedule.py +2 -1
- qulab/executor/utils.py +2 -0
- qulab/fun.cpython-310-darwin.so +0 -0
- qulab/version.py +1 -1
- qulab/executor/__main__.py +0 -200
- {QuLab-2.4.13.dist-info → QuLab-2.4.15.dist-info}/LICENSE +0 -0
- {QuLab-2.4.13.dist-info → QuLab-2.4.15.dist-info}/WHEEL +0 -0
- {QuLab-2.4.13.dist-info → QuLab-2.4.15.dist-info}/entry_points.txt +0 -0
- {QuLab-2.4.13.dist-info → QuLab-2.4.15.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
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-310-darwin.so,sha256=
|
|
5
|
-
qulab/version.py,sha256=
|
|
4
|
+
qulab/fun.cpython-310-darwin.so,sha256=cTaVtCuQtSu53XooP6NYgZR_6zBKaLaSZDSpPWHw3wk,126864
|
|
5
|
+
qulab/version.py,sha256=QW2MBz1JeuFtoyMh-qAewXiVGQGFzZAiBx69V3wIX5U,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/schedule.py,sha256=
|
|
9
|
+
qulab/executor/cli.py,sha256=LRSb5-J8Tn7dj-fxwCp3OP_6YUgPRoY-f8O18sbWG8I,5164
|
|
10
|
+
qulab/executor/load.py,sha256=4qQrw9O5dmWYYwxWt15z7uc6B3YuRB7xKgKi0g8-YtA,10418
|
|
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
|
|
12
|
-
qulab/executor/utils.py,sha256=
|
|
14
|
+
qulab/executor/utils.py,sha256=jp3F0swO9gpKhmTauxbgEK9WVorjI9FAL6zT60cYTdk,3106
|
|
13
15
|
qulab/monitor/__init__.py,sha256=nTHelnDpxRS_fl_B38TsN0njgq8eVTEz9IAnN3NbDlM,42
|
|
14
16
|
qulab/monitor/__main__.py,sha256=w3yUcqq195LzSnXTkQcuC1RSFRhy4oQ_PEBmucXguME,97
|
|
15
17
|
qulab/monitor/config.py,sha256=fQ5JcsMApKc1UwANEnIvbDQZl8uYW0tle92SaYtX9lI,744
|
|
@@ -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.15.dist-info/LICENSE,sha256=PRzIKxZtpQcH7whTG6Egvzl1A0BvnSf30tmR2X2KrpA,1065
|
|
95
|
+
QuLab-2.4.15.dist-info/METADATA,sha256=EgZbZvkhzL5UTZFxqJ2yHVir2RSvAkmSCNd_nUgitdE,3699
|
|
96
|
+
QuLab-2.4.15.dist-info/WHEEL,sha256=Yd3eJSBM2hj8W-ouaiMfFUwQYAS-D6P73Ob9yN5MZd0,114
|
|
97
|
+
QuLab-2.4.15.dist-info/entry_points.txt,sha256=b0v1GXOwmxY-nCCsPN_rHZZvY9CtTbWqrGj8u1m8yHo,45
|
|
98
|
+
QuLab-2.4.15.dist-info/top_level.txt,sha256=3T886LbAsbvjonu_TDdmgxKYUn939BVTRPxPl9r4cEg,6
|
|
99
|
+
QuLab-2.4.15.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
|
@@ -119,6 +119,17 @@ def is_workflow(module: ModuleType) -> bool:
|
|
|
119
119
|
return False
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def is_template(path: str | Path) -> bool:
|
|
123
|
+
path = Path(path)
|
|
124
|
+
if path.name == 'template.py':
|
|
125
|
+
return True
|
|
126
|
+
if path.name.endswith('_template.py'):
|
|
127
|
+
return True
|
|
128
|
+
if 'templates' in path.parts:
|
|
129
|
+
return True
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
122
133
|
def find_unreferenced_workflows(path: str) -> list[str]:
|
|
123
134
|
root = Path(path).resolve()
|
|
124
135
|
workflows = []
|
|
@@ -128,15 +139,14 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
128
139
|
for file_path in root.rglob("*.py"):
|
|
129
140
|
if file_path.name == "__init__.py":
|
|
130
141
|
continue
|
|
131
|
-
if file_path
|
|
132
|
-
"_template.py"):
|
|
142
|
+
if is_template(file_path):
|
|
133
143
|
continue
|
|
134
144
|
try:
|
|
135
145
|
rel_path = file_path.relative_to(root)
|
|
136
146
|
except ValueError:
|
|
137
147
|
continue
|
|
138
148
|
|
|
139
|
-
module =
|
|
149
|
+
module = load_workflow_from_file(str(rel_path), root)
|
|
140
150
|
|
|
141
151
|
if is_workflow(module):
|
|
142
152
|
rel_str = str(rel_path)
|
|
@@ -147,7 +157,7 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
147
157
|
|
|
148
158
|
# Check dependencies for each workflow module
|
|
149
159
|
for rel_str in workflows:
|
|
150
|
-
module =
|
|
160
|
+
module = load_workflow_from_file(rel_str, root)
|
|
151
161
|
|
|
152
162
|
depends_func = getattr(module, "depends", None)
|
|
153
163
|
if depends_func and callable(depends_func):
|
|
@@ -157,7 +167,9 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
157
167
|
)
|
|
158
168
|
continue
|
|
159
169
|
try:
|
|
160
|
-
depends_list = [
|
|
170
|
+
depends_list = [
|
|
171
|
+
n.__workflow_id__ for n in get_dependents(module, root)
|
|
172
|
+
]
|
|
161
173
|
except Exception as e:
|
|
162
174
|
warnings.warn(f"Error calling depends() in {rel_str}: {e}")
|
|
163
175
|
continue
|
|
@@ -183,11 +195,9 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
183
195
|
return unreferenced
|
|
184
196
|
|
|
185
197
|
|
|
186
|
-
def
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if file_name.startswith('cfg:'):
|
|
190
|
-
return SetConfigWorkflow(file_name[4:])
|
|
198
|
+
def load_workflow_from_file(file_name: str,
|
|
199
|
+
base_path: str | Path,
|
|
200
|
+
package='workflows') -> WorkflowType:
|
|
191
201
|
base_path = Path(base_path)
|
|
192
202
|
path = Path(file_name)
|
|
193
203
|
module_name = f"{package}.{'.'.join([*path.parts[:-1], path.stem])}"
|
|
@@ -195,6 +205,9 @@ def load_workflow_from_module(file_name: str,
|
|
|
195
205
|
module = module_from_spec(spec)
|
|
196
206
|
spec.loader.exec_module(module)
|
|
197
207
|
|
|
208
|
+
if hasattr(module, 'entries'):
|
|
209
|
+
return module
|
|
210
|
+
|
|
198
211
|
if not hasattr(module, '__timeout__'):
|
|
199
212
|
module.__timeout__ = None
|
|
200
213
|
|
|
@@ -207,37 +220,63 @@ def load_workflow_from_module(file_name: str,
|
|
|
207
220
|
return module
|
|
208
221
|
|
|
209
222
|
|
|
210
|
-
def load_workflow_from_template(
|
|
223
|
+
def load_workflow_from_template(template_path: str,
|
|
211
224
|
mappping: dict[str, str],
|
|
212
225
|
base_path: str | Path,
|
|
213
|
-
|
|
226
|
+
target_path: str | None = None,
|
|
214
227
|
package='workflows') -> WorkflowType:
|
|
215
228
|
base_path = Path(base_path)
|
|
216
|
-
path = Path(
|
|
229
|
+
path = Path(template_path)
|
|
217
230
|
|
|
218
231
|
with open(base_path / path) as f:
|
|
219
232
|
content = f.read()
|
|
220
|
-
|
|
233
|
+
|
|
234
|
+
def replace(text):
|
|
235
|
+
"""
|
|
236
|
+
将给定文本中的所有 _D_("var") 替换为 ${var}。
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
text (str): 包含 _D_ 调用的字符串。
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
str: 已经替换的新字符串。
|
|
243
|
+
"""
|
|
244
|
+
pattern = re.compile(r'_D_\s*\(\s*(["\'])(\w+)\1\s*\)')
|
|
245
|
+
replacement = r'${\2}'
|
|
246
|
+
new_text = re.sub(pattern, replacement, text)
|
|
247
|
+
return new_text
|
|
248
|
+
|
|
249
|
+
template = string.Template(replace(content))
|
|
221
250
|
content = template.substitute(mappping)
|
|
222
251
|
|
|
223
252
|
hash_str = hashlib.md5(pickle.dumps(mappping)).hexdigest()[:8]
|
|
224
|
-
if
|
|
253
|
+
if target_path is None:
|
|
225
254
|
if path.stem == 'template':
|
|
226
255
|
path = path.parent / f'tmp{hash_str}.py'
|
|
227
|
-
|
|
256
|
+
elif path.stem.endswith('_template'):
|
|
228
257
|
path = path.parent / path.stem.replace('_template',
|
|
229
258
|
f'_tmp{hash_str}.py')
|
|
230
|
-
else:
|
|
231
|
-
if path.stem == 'template':
|
|
232
|
-
path = path.parent / f'{subtitle}.py'
|
|
233
259
|
else:
|
|
234
|
-
path = path.parent / path.stem.
|
|
235
|
-
|
|
260
|
+
path = path.parent / f'{path.stem}_tmp{hash_str}.py'
|
|
261
|
+
else:
|
|
262
|
+
path = target_path
|
|
236
263
|
|
|
237
|
-
|
|
238
|
-
|
|
264
|
+
file = base_path / path
|
|
265
|
+
if not file.exists():
|
|
266
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
with open(file, 'w') as f:
|
|
268
|
+
f.write(content)
|
|
269
|
+
else:
|
|
270
|
+
if file.stat().st_mtime < Path(template_path).stat().st_mtime:
|
|
271
|
+
with open(file, 'w') as f:
|
|
272
|
+
f.write(content)
|
|
273
|
+
else:
|
|
274
|
+
if file.read_text() != content:
|
|
275
|
+
logger.warning(
|
|
276
|
+
f"`{file}` already exists and is different from the new one generated from template `{template_path}`"
|
|
277
|
+
)
|
|
239
278
|
|
|
240
|
-
module =
|
|
279
|
+
module = load_workflow_from_file(str(path), base_path, package)
|
|
241
280
|
|
|
242
281
|
return module
|
|
243
282
|
|
|
@@ -251,9 +290,9 @@ def load_workflow(workflow: str | tuple[str, dict],
|
|
|
251
290
|
w = load_workflow_from_template(file_name, mapping, base_path,
|
|
252
291
|
None, package)
|
|
253
292
|
elif len(workflow) == 3:
|
|
254
|
-
|
|
255
|
-
w = load_workflow_from_template(
|
|
256
|
-
|
|
293
|
+
template_path, target_path, mapping = workflow
|
|
294
|
+
w = load_workflow_from_template(template_path, mapping, base_path,
|
|
295
|
+
target_path, package)
|
|
257
296
|
else:
|
|
258
297
|
raise ValueError(f"Invalid workflow: {workflow}")
|
|
259
298
|
w.__workflow_id__ = str(Path(w.__file__).relative_to(base_path))
|
|
@@ -263,7 +302,7 @@ def load_workflow(workflow: str | tuple[str, dict],
|
|
|
263
302
|
w = SetConfigWorkflow(key)
|
|
264
303
|
w.__workflow_id__ = workflow
|
|
265
304
|
else:
|
|
266
|
-
w =
|
|
305
|
+
w = load_workflow_from_file(workflow, base_path, package)
|
|
267
306
|
w.__workflow_id__ = str(Path(w.__file__).relative_to(base_path))
|
|
268
307
|
else:
|
|
269
308
|
raise TypeError(f"Invalid workflow: {workflow}")
|
|
@@ -274,3 +313,7 @@ def load_workflow(workflow: str | tuple[str, dict],
|
|
|
274
313
|
def get_dependents(workflow: WorkflowType,
|
|
275
314
|
code_path: str | Path) -> list[WorkflowType]:
|
|
276
315
|
return [load_workflow(n, code_path) for n in workflow.depends()[0]]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_entries(workflow: WorkflowType, code_path: str | Path) -> WorkflowType:
|
|
319
|
+
return [load_workflow(n, code_path) for n in workflow.entries()]
|
qulab/executor/schedule.py
CHANGED
|
@@ -289,8 +289,9 @@ def run(workflow: WorkflowType,
|
|
|
289
289
|
code_path: str | Path,
|
|
290
290
|
state_path: str | Path,
|
|
291
291
|
plot: bool = False):
|
|
292
|
+
session_id = uuid.uuid4().hex
|
|
292
293
|
logger.debug(f'run "{workflow.__workflow_id__}" without dependences.')
|
|
293
|
-
result = calibrate(workflow, code_path, state_path, plot)
|
|
294
|
+
result = calibrate(workflow, code_path, state_path, plot, session_id=session_id)
|
|
294
295
|
if result.bad_data or not result.in_spec:
|
|
295
296
|
raise CalibrationFailedError(
|
|
296
297
|
f'"{workflow.__workflow_id__}": All dependents passed, but calibration failed!'
|
qulab/executor/utils.py
CHANGED
qulab/fun.cpython-310-darwin.so
CHANGED
|
Binary file
|
qulab/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.4.
|
|
1
|
+
__version__ = "2.4.15"
|
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
|