QuLab 2.4.9__tar.gz → 2.4.11__tar.gz
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.9 → qulab-2.4.11}/PKG-INFO +1 -1
- {qulab-2.4.9 → qulab-2.4.11}/QuLab.egg-info/PKG-INFO +1 -1
- {qulab-2.4.9 → qulab-2.4.11}/qulab/__main__.py +3 -1
- qulab-2.4.11/qulab/executor/__main__.py +200 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/executor/load.py +73 -11
- qulab-2.4.11/qulab/executor/schedule.py +304 -0
- qulab-2.4.11/qulab/version.py +1 -0
- qulab-2.4.9/qulab/executor/__main__.py +0 -92
- qulab-2.4.9/qulab/executor/schedule.py +0 -249
- qulab-2.4.9/qulab/version.py +0 -1
- {qulab-2.4.9 → qulab-2.4.11}/LICENSE +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/MANIFEST.in +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/QuLab.egg-info/SOURCES.txt +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/QuLab.egg-info/dependency_links.txt +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/QuLab.egg-info/entry_points.txt +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/QuLab.egg-info/requires.txt +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/QuLab.egg-info/top_level.txt +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/README.md +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/pyproject.toml +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/dicttree.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/executor/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/executor/storage.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/executor/transform.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/executor/utils.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/__main__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/config.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/dataset.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/event_queue.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/mainwindow.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/monitor.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/ploter.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/qt_compat.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/monitor/toolbar.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/curd.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/expression.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/models.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/optimize.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/query.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/record.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/scan.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/server.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/space.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/scan/utils.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/__main__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/backend/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/backend/redis.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/base_dataset.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/chunk.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/dataset.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/file.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/base.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/config.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/file.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/ipy.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/models.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/record.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/report.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/models/tag.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/storage/storage.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/chat.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/device/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/device/basedevice.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/device/loader.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/device/utils.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/drivers/FakeInstrument.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/drivers/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/ipy_events.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/bencoder.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/cli.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/dhcp.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/dhcpd.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/kad.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/kcp.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/net/nginx.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/progress.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/client.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/exceptions.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/msgpack.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/msgpack.pyi +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/router.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/rpc.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/serialize.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/server.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/socket.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/utils.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/worker.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/sys/rpc/zmq_socket.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/__init__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/__main__.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/_autoplot.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/plot_circ.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/plot_layout.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/plot_seq.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/qdat.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/rot3d.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/qulab/visualization/widgets.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/setup.cfg +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/setup.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/src/qulab.h +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/tests/test_kad.py +0 -0
- {qulab-2.4.9 → qulab-2.4.11}/tests/test_scan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
|
-
from .executor.__main__ import create, maintain, run
|
|
3
|
+
from .executor.__main__ import create, get, maintain, run, set
|
|
4
4
|
from .monitor.__main__ import main as monitor
|
|
5
5
|
from .scan.server import server
|
|
6
6
|
from .sys.net.cli import dht
|
|
@@ -25,6 +25,8 @@ cli.add_command(server)
|
|
|
25
25
|
cli.add_command(maintain)
|
|
26
26
|
cli.add_command(run)
|
|
27
27
|
cli.add_command(create)
|
|
28
|
+
cli.add_command(set)
|
|
29
|
+
cli.add_command(get)
|
|
28
30
|
|
|
29
31
|
if __name__ == '__main__':
|
|
30
32
|
cli()
|
|
@@ -0,0 +1,200 @@
|
|
|
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()
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import inspect
|
|
3
|
+
import pickle
|
|
4
|
+
import re
|
|
5
|
+
import string
|
|
2
6
|
import warnings
|
|
3
7
|
from importlib.util import module_from_spec, spec_from_file_location
|
|
4
8
|
from pathlib import Path
|
|
5
9
|
from types import ModuleType
|
|
6
10
|
|
|
7
|
-
import
|
|
11
|
+
from loguru import logger
|
|
8
12
|
|
|
9
13
|
from .storage import Result
|
|
10
14
|
|
|
@@ -124,12 +128,15 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
124
128
|
for file_path in root.rglob("*.py"):
|
|
125
129
|
if file_path.name == "__init__.py":
|
|
126
130
|
continue
|
|
131
|
+
if file_path.name.endswith("_template.py") or re.match(
|
|
132
|
+
r'.*_tmp_[0-9a-fA-F]{8}.py', file_path.name):
|
|
133
|
+
continue
|
|
127
134
|
try:
|
|
128
135
|
rel_path = file_path.relative_to(root)
|
|
129
136
|
except ValueError:
|
|
130
137
|
continue
|
|
131
138
|
|
|
132
|
-
module =
|
|
139
|
+
module = load_workflow_from_module(str(rel_path), root)
|
|
133
140
|
|
|
134
141
|
if is_workflow(module):
|
|
135
142
|
rel_str = str(rel_path)
|
|
@@ -140,7 +147,7 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
140
147
|
|
|
141
148
|
# Check dependencies for each workflow module
|
|
142
149
|
for rel_str in workflows:
|
|
143
|
-
module =
|
|
150
|
+
module = load_workflow_from_module(rel_str, root)
|
|
144
151
|
|
|
145
152
|
depends_func = getattr(module, "depends", None)
|
|
146
153
|
if depends_func and callable(depends_func):
|
|
@@ -150,17 +157,15 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
150
157
|
)
|
|
151
158
|
continue
|
|
152
159
|
try:
|
|
153
|
-
depends_list = depends_func()
|
|
160
|
+
depends_list = depends_func()[0]
|
|
154
161
|
except Exception as e:
|
|
155
162
|
warnings.warn(f"Error calling depends() in {rel_str}: {e}")
|
|
156
163
|
continue
|
|
157
164
|
|
|
158
165
|
if not isinstance(depends_list, list) or not all(
|
|
159
|
-
|
|
160
|
-
):
|
|
166
|
+
isinstance(item, str) for item in depends_list):
|
|
161
167
|
warnings.warn(
|
|
162
|
-
f"depends() in {rel_str} did not return a list of strings"
|
|
163
|
-
)
|
|
168
|
+
f"depends() in {rel_str} did not return a list of strings")
|
|
164
169
|
continue
|
|
165
170
|
|
|
166
171
|
for dep in depends_list:
|
|
@@ -178,9 +183,9 @@ def find_unreferenced_workflows(path: str) -> list[str]:
|
|
|
178
183
|
return unreferenced
|
|
179
184
|
|
|
180
185
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
def load_workflow_from_module(file_name: str,
|
|
187
|
+
base_path: str | Path,
|
|
188
|
+
package='workflows') -> WorkflowType:
|
|
184
189
|
if file_name.startswith('cfg:'):
|
|
185
190
|
return SetConfigWorkflow(file_name[4:])
|
|
186
191
|
base_path = Path(base_path)
|
|
@@ -200,3 +205,60 @@ def load_workflow(file_name: str,
|
|
|
200
205
|
verify_check_method(module)
|
|
201
206
|
|
|
202
207
|
return module
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def load_workflow_from_template(file_name: str,
|
|
211
|
+
mappping: dict[str, str],
|
|
212
|
+
base_path: str | Path,
|
|
213
|
+
subtitle: str | None = None,
|
|
214
|
+
package='workflows') -> WorkflowType:
|
|
215
|
+
base_path = Path(base_path)
|
|
216
|
+
path = Path(file_name)
|
|
217
|
+
|
|
218
|
+
with open(base_path / path) as f:
|
|
219
|
+
content = f.read()
|
|
220
|
+
template = string.Template(content)
|
|
221
|
+
content = template.substitute(mappping)
|
|
222
|
+
|
|
223
|
+
hash_str = hashlib.md5(pickle.dumps(mappping)).hexdigest()[:8]
|
|
224
|
+
if subtitle is None:
|
|
225
|
+
path = path.parent / path.stem.replace('_template',
|
|
226
|
+
f'_tmp{hash_str}.py')
|
|
227
|
+
else:
|
|
228
|
+
path = path.parent / path.stem.replace('_template', f'_{subtitle}.py')
|
|
229
|
+
|
|
230
|
+
with open(base_path / path, 'w') as f:
|
|
231
|
+
f.write(content)
|
|
232
|
+
|
|
233
|
+
module = load_workflow_from_module(str(path), base_path, package)
|
|
234
|
+
|
|
235
|
+
return module
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def load_workflow(workflow: str | tuple[str, dict],
|
|
239
|
+
base_path: str | Path,
|
|
240
|
+
package='workflows') -> WorkflowType:
|
|
241
|
+
if isinstance(workflow, tuple):
|
|
242
|
+
if len(workflow) == 2:
|
|
243
|
+
file_name, mapping = workflow
|
|
244
|
+
w = load_workflow_from_template(file_name, mapping, base_path,
|
|
245
|
+
None, package)
|
|
246
|
+
elif len(workflow) == 3:
|
|
247
|
+
file_name, subtitle, mapping = workflow
|
|
248
|
+
w = load_workflow_from_template(file_name, mapping, base_path,
|
|
249
|
+
subtitle, package)
|
|
250
|
+
else:
|
|
251
|
+
raise ValueError(f"Invalid workflow: {workflow}")
|
|
252
|
+
w.__workflow_id__ = str(Path(w.__file__).relative_to(base_path))
|
|
253
|
+
elif isinstance(workflow, str):
|
|
254
|
+
if workflow.startswith('cfg:'):
|
|
255
|
+
key = workflow[4:]
|
|
256
|
+
w = SetConfigWorkflow(key)
|
|
257
|
+
w.__workflow_id__ = workflow
|
|
258
|
+
else:
|
|
259
|
+
w = load_workflow_from_module(workflow, base_path, package)
|
|
260
|
+
w.__workflow_id__ = str(Path(w.__file__).relative_to(base_path))
|
|
261
|
+
else:
|
|
262
|
+
raise TypeError(f"Invalid workflow: {workflow}")
|
|
263
|
+
|
|
264
|
+
return w
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from . import transform
|
|
9
|
+
from .load import WorkflowType, load_workflow
|
|
10
|
+
from .storage import (Result, find_result, get_head, renew_result,
|
|
11
|
+
revoke_result, save_result)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CalibrationFailedError(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_state(workflow: WorkflowType, code_path: str | Path,
|
|
19
|
+
state_path: str | Path) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
check state should report a pass if and only if the following are satisfied:
|
|
22
|
+
|
|
23
|
+
1. The cal has had check data or calibrate pass within the timeout period.
|
|
24
|
+
2. The cal has not failed calibrate without resolution.
|
|
25
|
+
3. No dependencies have been recalibrated since the last time check data or calibrate was run on this cal.
|
|
26
|
+
4. All dependencies pass check state.
|
|
27
|
+
"""
|
|
28
|
+
logger.debug(f'check_state: "{workflow}"')
|
|
29
|
+
result = find_result(workflow.__workflow_id__, state_path)
|
|
30
|
+
if not result:
|
|
31
|
+
logger.debug(
|
|
32
|
+
f'check_state failed: No history found for "{workflow.__workflow_id__}"'
|
|
33
|
+
)
|
|
34
|
+
return False
|
|
35
|
+
if hasattr(workflow, 'check_state') and callable(workflow.check_state):
|
|
36
|
+
logger.debug(
|
|
37
|
+
f'check_state: "{workflow.__workflow_id__}" has custom check_state method'
|
|
38
|
+
)
|
|
39
|
+
return workflow.check_state(result)
|
|
40
|
+
if workflow.__timeout__ is not None and datetime.now(
|
|
41
|
+
) > result.checked_time + timedelta(seconds=workflow.__timeout__):
|
|
42
|
+
logger.debug(
|
|
43
|
+
f'check_state failed: "{workflow.__workflow_id__}" has expired')
|
|
44
|
+
return False
|
|
45
|
+
if not result.in_spec:
|
|
46
|
+
logger.debug(
|
|
47
|
+
f'check_state failed: "{workflow.__workflow_id__}" is out of spec')
|
|
48
|
+
return False
|
|
49
|
+
if result.bad_data:
|
|
50
|
+
logger.debug(
|
|
51
|
+
f'check_state failed: "{workflow.__workflow_id__}" has bad data')
|
|
52
|
+
return False
|
|
53
|
+
for n in get_dependents(workflow, code_path):
|
|
54
|
+
r = find_result(n, state_path)
|
|
55
|
+
if r is None or r.checked_time > result.checked_time:
|
|
56
|
+
logger.debug(
|
|
57
|
+
f'check_state failed: "{workflow.__workflow_id__}" has outdated dependencies'
|
|
58
|
+
)
|
|
59
|
+
return False
|
|
60
|
+
for n in get_dependents(workflow, code_path):
|
|
61
|
+
if not check_state(n, code_path, state_path):
|
|
62
|
+
logger.debug(
|
|
63
|
+
f'check_state failed: "{workflow.__workflow_id__}" has bad dependencies'
|
|
64
|
+
)
|
|
65
|
+
return False
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def call_analyzer(node, data, history, check=False, plot=False):
|
|
70
|
+
if check:
|
|
71
|
+
result = transform.params_to_result(
|
|
72
|
+
node.check_analyze(*data,
|
|
73
|
+
history=transform.result_to_params(history)))
|
|
74
|
+
result.fully_calibrated = False
|
|
75
|
+
else:
|
|
76
|
+
result = transform.params_to_result(
|
|
77
|
+
node.analyze(*data, history=transform.result_to_params(history)))
|
|
78
|
+
result.fully_calibrated = True
|
|
79
|
+
if plot:
|
|
80
|
+
call_plot(node, result)
|
|
81
|
+
result.data = data
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@logger.catch()
|
|
86
|
+
def call_plot(node, result, check=False):
|
|
87
|
+
if hasattr(node, 'plot') and callable(node.plot):
|
|
88
|
+
state, params, other = transform.result_to_params(result)
|
|
89
|
+
node.plot(state, params, other)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@functools.lru_cache(maxsize=128)
|
|
93
|
+
def check_data(workflow: WorkflowType, code_path: str | Path,
|
|
94
|
+
state_path: str | Path, plot: bool, session_id: str) -> Result:
|
|
95
|
+
"""
|
|
96
|
+
check data answers two questions:
|
|
97
|
+
Is the parameter associated with this cal in spec,
|
|
98
|
+
and is the cal scan working as expected?
|
|
99
|
+
"""
|
|
100
|
+
history = find_result(workflow.__workflow_id__, state_path)
|
|
101
|
+
|
|
102
|
+
if history is None:
|
|
103
|
+
logger.debug(f'No history found for "{workflow.__workflow_id__}"')
|
|
104
|
+
result = Result()
|
|
105
|
+
result.in_spec = False
|
|
106
|
+
result.bad_data = False
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
if history.bad_data:
|
|
110
|
+
logger.debug(
|
|
111
|
+
f'History found for "{workflow.__workflow_id__}", but bad data')
|
|
112
|
+
return history
|
|
113
|
+
if not history.in_spec:
|
|
114
|
+
logger.debug(
|
|
115
|
+
f'History found for "{workflow.__workflow_id__}", but out of spec')
|
|
116
|
+
return history
|
|
117
|
+
|
|
118
|
+
logger.debug(
|
|
119
|
+
f'History found for "{workflow.__workflow_id__}", but has expired')
|
|
120
|
+
|
|
121
|
+
if hasattr(workflow, 'check') and callable(workflow.check) and hasattr(
|
|
122
|
+
workflow, 'check_analyze') and callable(workflow.check_analyze):
|
|
123
|
+
logger.debug(f'Checking "{workflow}" with "check" method ...')
|
|
124
|
+
data = workflow.check()
|
|
125
|
+
result = Result()
|
|
126
|
+
result.data = data
|
|
127
|
+
save_result(workflow.__workflow_id__, result, state_path)
|
|
128
|
+
|
|
129
|
+
logger.debug(f'Checked "{workflow}" !')
|
|
130
|
+
result = call_analyzer(workflow, data, history, check=True, plot=plot)
|
|
131
|
+
if result.in_spec:
|
|
132
|
+
logger.debug(
|
|
133
|
+
f'"{workflow.__workflow_id__}": checked in spec, renewing result'
|
|
134
|
+
)
|
|
135
|
+
renew_result(workflow.__workflow_id__, state_path)
|
|
136
|
+
else:
|
|
137
|
+
logger.debug(
|
|
138
|
+
f'"{workflow.__workflow_id__}": checked out of spec, revoking result'
|
|
139
|
+
)
|
|
140
|
+
revoke_result(workflow.__workflow_id__, state_path)
|
|
141
|
+
else:
|
|
142
|
+
logger.debug(
|
|
143
|
+
f'Checking "{workflow.__workflow_id__}" with "calibrate" method ...'
|
|
144
|
+
)
|
|
145
|
+
data = workflow.calibrate()
|
|
146
|
+
result = Result()
|
|
147
|
+
result.data = data
|
|
148
|
+
save_result(workflow.__workflow_id__, result, state_path)
|
|
149
|
+
|
|
150
|
+
logger.debug(f'Calibrated "{workflow}" !')
|
|
151
|
+
result = call_analyzer(workflow, data, history, check=False, plot=plot)
|
|
152
|
+
save_result(workflow.__workflow_id__, result, state_path,
|
|
153
|
+
get_head(workflow.__workflow_id__, state_path))
|
|
154
|
+
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@functools.lru_cache(maxsize=128)
|
|
159
|
+
def calibrate(workflow: WorkflowType, code_path: str | Path,
|
|
160
|
+
state_path: str | Path, plot: bool, session_id: str) -> Result:
|
|
161
|
+
history = find_result(workflow, state_path)
|
|
162
|
+
|
|
163
|
+
logger.debug(f'Calibrating "{workflow.__workflow_id__}" ...')
|
|
164
|
+
data = workflow.calibrate()
|
|
165
|
+
result = Result()
|
|
166
|
+
result.data = data
|
|
167
|
+
save_result(workflow.__workflow_id__, result, state_path)
|
|
168
|
+
logger.debug(f'Calibrated "{workflow.__workflow_id__}" !')
|
|
169
|
+
result = call_analyzer(workflow, data, history, check=False, plot=plot)
|
|
170
|
+
save_result(workflow.__workflow_id__, result, state_path,
|
|
171
|
+
get_head(workflow.__workflow_id__, state_path))
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def diagnose(workflow: WorkflowType, code_path: str | Path,
|
|
176
|
+
state_path: str | Path, plot: bool, session_id: str):
|
|
177
|
+
'''
|
|
178
|
+
Returns: True if node or dependent recalibrated.
|
|
179
|
+
'''
|
|
180
|
+
logger.debug(f'diagnose "{workflow.__workflow_id__}"')
|
|
181
|
+
# check_data
|
|
182
|
+
result = check_data(workflow, code_path, state_path, plot, session_id)
|
|
183
|
+
# in spec case
|
|
184
|
+
if result.in_spec:
|
|
185
|
+
logger.debug(
|
|
186
|
+
f'"{workflow.__workflow_id__}": Checked! In spec, no need to diagnose'
|
|
187
|
+
)
|
|
188
|
+
return False
|
|
189
|
+
# bad data case
|
|
190
|
+
recalibrated = []
|
|
191
|
+
if result.bad_data:
|
|
192
|
+
logger.debug(
|
|
193
|
+
f'"{workflow.__workflow_id__}": Bad data, diagnosing dependents')
|
|
194
|
+
recalibrated = [
|
|
195
|
+
diagnose(n, code_path, state_path, plot, session_id)
|
|
196
|
+
for n in get_dependents(workflow, code_path)
|
|
197
|
+
]
|
|
198
|
+
if not any(recalibrated):
|
|
199
|
+
if result.bad_data:
|
|
200
|
+
raise CalibrationFailedError(
|
|
201
|
+
f'"{workflow.__workflow_id__}": bad data but no dependents recalibrated.'
|
|
202
|
+
)
|
|
203
|
+
logger.debug(
|
|
204
|
+
f'"{workflow.__workflow_id__}": no dependents recalibrated.')
|
|
205
|
+
# calibrate
|
|
206
|
+
if any(recalibrated):
|
|
207
|
+
logger.debug(
|
|
208
|
+
f'recalibrate "{workflow.__workflow_id__}" because some dependents recalibrated.'
|
|
209
|
+
)
|
|
210
|
+
elif not result.in_spec and not result.bad_data:
|
|
211
|
+
logger.debug(
|
|
212
|
+
f'recalibrate "{workflow.__workflow_id__}" because out of spec.')
|
|
213
|
+
elif result.in_spec:
|
|
214
|
+
logger.error(
|
|
215
|
+
f'Never reach: recalibrate "{workflow.__workflow_id__}" because in spec.'
|
|
216
|
+
)
|
|
217
|
+
elif result.bad_data:
|
|
218
|
+
logger.error(
|
|
219
|
+
f'Never reach: recalibrate "{workflow.__workflow_id__}" because bad data.'
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
logger.error(f'Never reach: recalibrate "{workflow.__workflow_id__}"')
|
|
223
|
+
|
|
224
|
+
result = calibrate(workflow, code_path, state_path, plot, session_id)
|
|
225
|
+
if result.bad_data or not result.in_spec:
|
|
226
|
+
raise CalibrationFailedError(
|
|
227
|
+
f'"{workflow.__workflow_id__}": All dependents passed, but calibration failed!'
|
|
228
|
+
)
|
|
229
|
+
transform.update_parameters(result)
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_dependents(workflow: WorkflowType,
|
|
234
|
+
code_path: str | Path) -> list[WorkflowType]:
|
|
235
|
+
return [load_workflow(n, code_path) for n in workflow.depends()[0]]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@logger.catch(reraise=True)
|
|
239
|
+
def maintain(workflow: WorkflowType,
|
|
240
|
+
code_path: str | Path,
|
|
241
|
+
state_path: str | Path,
|
|
242
|
+
session_id: str | None = None,
|
|
243
|
+
run: bool = False,
|
|
244
|
+
plot: bool = False):
|
|
245
|
+
if session_id is None:
|
|
246
|
+
session_id = uuid.uuid4().hex
|
|
247
|
+
logger.debug(f'run "{workflow.__workflow_id__}"'
|
|
248
|
+
if run else f'maintain "{workflow.__workflow_id__}"')
|
|
249
|
+
# recursive maintain
|
|
250
|
+
for n in get_dependents(workflow, code_path):
|
|
251
|
+
logger.debug(
|
|
252
|
+
f'maintain "{n.__workflow_id__}" because it is depended by "{workflow.__workflow_id__}"'
|
|
253
|
+
)
|
|
254
|
+
maintain(n, code_path, state_path, session_id, run=False, plot=plot)
|
|
255
|
+
else:
|
|
256
|
+
logger.debug(
|
|
257
|
+
f'"{workflow.__workflow_id__}": All dependents maintained')
|
|
258
|
+
# check_state
|
|
259
|
+
if check_state(workflow, code_path, state_path) and not run:
|
|
260
|
+
logger.debug(
|
|
261
|
+
f'"{workflow.__workflow_id__}": In spec, no need to maintain')
|
|
262
|
+
return
|
|
263
|
+
# check_data
|
|
264
|
+
result = check_data(workflow, code_path, state_path, plot, session_id)
|
|
265
|
+
if result.in_spec:
|
|
266
|
+
if not run:
|
|
267
|
+
logger.debug(
|
|
268
|
+
f'"{workflow.__workflow_id__}": In spec, no need to maintain')
|
|
269
|
+
return
|
|
270
|
+
elif result.bad_data:
|
|
271
|
+
logger.debug(
|
|
272
|
+
f'"{workflow.__workflow_id__}": Bad data, diagnosing dependents')
|
|
273
|
+
for n in get_dependents(workflow, code_path):
|
|
274
|
+
logger.debug(
|
|
275
|
+
f'diagnose "{n.__workflow_id__}" because of "{workflow.__workflow_id__}" bad data'
|
|
276
|
+
)
|
|
277
|
+
diagnose(n, code_path, state_path, plot, session_id)
|
|
278
|
+
else:
|
|
279
|
+
logger.debug(
|
|
280
|
+
f'"{workflow.__workflow_id__}": All dependents diagnosed')
|
|
281
|
+
# calibrate
|
|
282
|
+
logger.debug(f'recalibrate "{workflow.__workflow_id__}"')
|
|
283
|
+
result = calibrate(workflow, code_path, state_path, plot, session_id)
|
|
284
|
+
if result.bad_data or not result.in_spec:
|
|
285
|
+
raise CalibrationFailedError(
|
|
286
|
+
f'"{workflow.__workflow_id__}": All dependents passed, but calibration failed!'
|
|
287
|
+
)
|
|
288
|
+
transform.update_parameters(result)
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@logger.catch(reraise=True)
|
|
293
|
+
def run(workflow: WorkflowType,
|
|
294
|
+
code_path: str | Path,
|
|
295
|
+
state_path: str | Path,
|
|
296
|
+
plot: bool = False):
|
|
297
|
+
logger.debug(f'run "{workflow.__workflow_id__}" without dependences.')
|
|
298
|
+
result = calibrate(workflow, code_path, state_path, plot)
|
|
299
|
+
if result.bad_data or not result.in_spec:
|
|
300
|
+
raise CalibrationFailedError(
|
|
301
|
+
f'"{workflow.__workflow_id__}": All dependents passed, but calibration failed!'
|
|
302
|
+
)
|
|
303
|
+
transform.update_parameters(result)
|
|
304
|
+
return
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.4.11"
|