QuLab 2.10.10__cp313-cp313-macosx_10_13_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/__init__.py +33 -0
- qulab/__main__.py +4 -0
- qulab/cli/__init__.py +0 -0
- qulab/cli/commands.py +30 -0
- qulab/cli/config.py +170 -0
- qulab/cli/decorators.py +28 -0
- qulab/dicttree.py +523 -0
- qulab/executor/__init__.py +5 -0
- qulab/executor/analyze.py +188 -0
- qulab/executor/cli.py +434 -0
- qulab/executor/load.py +563 -0
- qulab/executor/registry.py +185 -0
- qulab/executor/schedule.py +543 -0
- qulab/executor/storage.py +615 -0
- qulab/executor/template.py +259 -0
- qulab/executor/utils.py +194 -0
- qulab/expression.py +827 -0
- qulab/fun.cpython-313-darwin.so +0 -0
- qulab/monitor/__init__.py +1 -0
- qulab/monitor/__main__.py +8 -0
- qulab/monitor/config.py +41 -0
- qulab/monitor/dataset.py +77 -0
- qulab/monitor/event_queue.py +54 -0
- qulab/monitor/mainwindow.py +234 -0
- qulab/monitor/monitor.py +115 -0
- qulab/monitor/ploter.py +123 -0
- qulab/monitor/qt_compat.py +16 -0
- qulab/monitor/toolbar.py +265 -0
- qulab/scan/__init__.py +2 -0
- qulab/scan/curd.py +221 -0
- qulab/scan/models.py +554 -0
- qulab/scan/optimize.py +76 -0
- qulab/scan/query.py +387 -0
- qulab/scan/record.py +603 -0
- qulab/scan/scan.py +1166 -0
- qulab/scan/server.py +450 -0
- qulab/scan/space.py +213 -0
- qulab/scan/utils.py +234 -0
- qulab/storage/__init__.py +0 -0
- qulab/storage/__main__.py +51 -0
- qulab/storage/backend/__init__.py +0 -0
- qulab/storage/backend/redis.py +204 -0
- qulab/storage/base_dataset.py +352 -0
- qulab/storage/chunk.py +60 -0
- qulab/storage/dataset.py +127 -0
- qulab/storage/file.py +273 -0
- qulab/storage/models/__init__.py +22 -0
- qulab/storage/models/base.py +4 -0
- qulab/storage/models/config.py +28 -0
- qulab/storage/models/file.py +89 -0
- qulab/storage/models/ipy.py +58 -0
- qulab/storage/models/models.py +88 -0
- qulab/storage/models/record.py +161 -0
- qulab/storage/models/report.py +22 -0
- qulab/storage/models/tag.py +93 -0
- qulab/storage/storage.py +95 -0
- qulab/sys/__init__.py +2 -0
- qulab/sys/chat.py +688 -0
- qulab/sys/device/__init__.py +3 -0
- qulab/sys/device/basedevice.py +255 -0
- qulab/sys/device/loader.py +86 -0
- qulab/sys/device/utils.py +79 -0
- qulab/sys/drivers/FakeInstrument.py +68 -0
- qulab/sys/drivers/__init__.py +0 -0
- qulab/sys/ipy_events.py +125 -0
- qulab/sys/net/__init__.py +0 -0
- qulab/sys/net/bencoder.py +205 -0
- qulab/sys/net/cli.py +169 -0
- qulab/sys/net/dhcp.py +543 -0
- qulab/sys/net/dhcpd.py +176 -0
- qulab/sys/net/kad.py +1142 -0
- qulab/sys/net/kcp.py +192 -0
- qulab/sys/net/nginx.py +194 -0
- qulab/sys/progress.py +190 -0
- qulab/sys/rpc/__init__.py +0 -0
- qulab/sys/rpc/client.py +0 -0
- qulab/sys/rpc/exceptions.py +96 -0
- qulab/sys/rpc/msgpack.py +1052 -0
- qulab/sys/rpc/msgpack.pyi +41 -0
- qulab/sys/rpc/router.py +35 -0
- qulab/sys/rpc/rpc.py +412 -0
- qulab/sys/rpc/serialize.py +139 -0
- qulab/sys/rpc/server.py +29 -0
- qulab/sys/rpc/socket.py +29 -0
- qulab/sys/rpc/utils.py +25 -0
- qulab/sys/rpc/worker.py +0 -0
- qulab/sys/rpc/zmq_socket.py +227 -0
- qulab/tools/__init__.py +0 -0
- qulab/tools/connection_helper.py +39 -0
- qulab/typing.py +2 -0
- qulab/utils.py +95 -0
- qulab/version.py +1 -0
- qulab/visualization/__init__.py +188 -0
- qulab/visualization/__main__.py +71 -0
- qulab/visualization/_autoplot.py +464 -0
- qulab/visualization/plot_circ.py +319 -0
- qulab/visualization/plot_layout.py +408 -0
- qulab/visualization/plot_seq.py +242 -0
- qulab/visualization/qdat.py +152 -0
- qulab/visualization/rot3d.py +23 -0
- qulab/visualization/widgets.py +86 -0
- qulab-2.10.10.dist-info/METADATA +110 -0
- qulab-2.10.10.dist-info/RECORD +107 -0
- qulab-2.10.10.dist-info/WHEEL +5 -0
- qulab-2.10.10.dist-info/entry_points.txt +2 -0
- qulab-2.10.10.dist-info/licenses/LICENSE +21 -0
- qulab-2.10.10.dist-info/top_level.txt +1 -0
qulab/executor/load.py
ADDED
@@ -0,0 +1,563 @@
|
|
1
|
+
import atexit
|
2
|
+
import inspect
|
3
|
+
import os
|
4
|
+
import pickle
|
5
|
+
import shutil
|
6
|
+
import tempfile
|
7
|
+
import time
|
8
|
+
import warnings
|
9
|
+
from importlib.util import module_from_spec, spec_from_file_location
|
10
|
+
from pathlib import Path
|
11
|
+
from types import ModuleType
|
12
|
+
from typing import Any
|
13
|
+
|
14
|
+
from loguru import logger
|
15
|
+
|
16
|
+
from .registry import Registry
|
17
|
+
from .storage import Report
|
18
|
+
from .template import (TemplateKeyError, TemplateTypeError, decode_mapping,
|
19
|
+
inject_mapping)
|
20
|
+
|
21
|
+
|
22
|
+
class SetConfigWorkflow():
|
23
|
+
__timeout__ = None
|
24
|
+
__mtime__ = 0
|
25
|
+
__source__ = ''
|
26
|
+
|
27
|
+
def __init__(self, key):
|
28
|
+
self.key = key
|
29
|
+
self.__workflow_id__ = f"cfg:{self.key}"
|
30
|
+
|
31
|
+
def depends(self):
|
32
|
+
return []
|
33
|
+
|
34
|
+
def check_state(self, history: Report) -> bool:
|
35
|
+
reg = Registry()
|
36
|
+
try:
|
37
|
+
return self._equal(history.parameters[self.key], reg.get(self.key))
|
38
|
+
except:
|
39
|
+
return False
|
40
|
+
|
41
|
+
def calibrate(self):
|
42
|
+
reg = Registry()
|
43
|
+
try:
|
44
|
+
value = reg.get(self.key)
|
45
|
+
except:
|
46
|
+
value = eval(input(f'"{self.key}": '))
|
47
|
+
return value
|
48
|
+
|
49
|
+
def analyze(self, report: Report, history: Report):
|
50
|
+
report.state = 'OK'
|
51
|
+
report.parameters = {self.key: report.data}
|
52
|
+
return report
|
53
|
+
|
54
|
+
def check(self):
|
55
|
+
return self.calibrate()
|
56
|
+
|
57
|
+
def check_analyze(self, report: Report, history: Report | None):
|
58
|
+
if self.check_state(history):
|
59
|
+
report.state = 'OK'
|
60
|
+
report.parameters = {self.key: history.data}
|
61
|
+
else:
|
62
|
+
report.state = 'Outdated'
|
63
|
+
return report
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def _equal(a, b):
|
67
|
+
import numpy as np
|
68
|
+
|
69
|
+
if a is b:
|
70
|
+
return True
|
71
|
+
try:
|
72
|
+
return a == b
|
73
|
+
except:
|
74
|
+
pass
|
75
|
+
|
76
|
+
if isinstance(a, np.ndarray) and isinstance(b, np.ndarray):
|
77
|
+
return a.shape == b.shape and np.all(a == b)
|
78
|
+
|
79
|
+
return False
|
80
|
+
|
81
|
+
def __hash__(self):
|
82
|
+
return hash(self.__workflow_id__)
|
83
|
+
|
84
|
+
|
85
|
+
WorkflowType = ModuleType | SetConfigWorkflow
|
86
|
+
|
87
|
+
|
88
|
+
def can_call_without_args(func):
|
89
|
+
if not callable(func):
|
90
|
+
return False
|
91
|
+
|
92
|
+
# 获取函数签名
|
93
|
+
sig = inspect.signature(func)
|
94
|
+
for param in sig.parameters.values():
|
95
|
+
# 如果有参数没有默认值且不是可变参数,则无法无参调用
|
96
|
+
if (param.default is param.empty and param.kind
|
97
|
+
not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)):
|
98
|
+
return False
|
99
|
+
return True
|
100
|
+
|
101
|
+
|
102
|
+
def verify_calibrate_method(module: WorkflowType):
|
103
|
+
if not hasattr(module, 'calibrate'):
|
104
|
+
raise AttributeError(
|
105
|
+
f"Workflow {module.__file__} does not have 'calibrate' function")
|
106
|
+
|
107
|
+
if not can_call_without_args(module.calibrate):
|
108
|
+
raise AttributeError(
|
109
|
+
f"Workflow {module.__file__} 'calibrate' function should not have any parameters"
|
110
|
+
)
|
111
|
+
|
112
|
+
if not hasattr(module, 'analyze'):
|
113
|
+
raise AttributeError(
|
114
|
+
f"Workflow {module.__file__} does not have 'analyze' function")
|
115
|
+
|
116
|
+
|
117
|
+
def verify_check_method(module: WorkflowType):
|
118
|
+
if not hasattr(module, 'check'):
|
119
|
+
warnings.warn(
|
120
|
+
f"Workflow {module.__file__} does not have 'check' function, it will be set to 'calibrate' function"
|
121
|
+
)
|
122
|
+
else:
|
123
|
+
if not can_call_without_args(module.check):
|
124
|
+
raise AttributeError(
|
125
|
+
f"Workflow {module.__file__} 'check' function should not have any parameters"
|
126
|
+
)
|
127
|
+
|
128
|
+
if not hasattr(module, 'check_analyze'):
|
129
|
+
raise AttributeError(
|
130
|
+
f"Workflow {module.__file__} has 'check' function but does not have 'check_analyze' function"
|
131
|
+
)
|
132
|
+
|
133
|
+
|
134
|
+
def verify_dependence_key(workflow: str | tuple[str, dict[str, Any]]
|
135
|
+
| tuple[str, str, dict[str, Any]], base_path: Path):
|
136
|
+
if isinstance(workflow, str):
|
137
|
+
return
|
138
|
+
if not isinstance(workflow, tuple) or len(workflow) not in [2, 3]:
|
139
|
+
raise ValueError(f"Invalid workflow: {workflow}")
|
140
|
+
|
141
|
+
if len(workflow) == 2 and isinstance(workflow[1], str):
|
142
|
+
file_name, mapping = workflow
|
143
|
+
if not Path(file_name).exists():
|
144
|
+
raise FileNotFoundError(f"File not found: {file_name}")
|
145
|
+
elif len(workflow) == 2 and isinstance(workflow[1], dict):
|
146
|
+
template_path, mapping = workflow
|
147
|
+
if not (Path(base_path) / template_path).exists():
|
148
|
+
raise FileNotFoundError(f"File not found: {template_path}")
|
149
|
+
elif len(workflow) == 3:
|
150
|
+
template_path, target_path, mapping = workflow
|
151
|
+
if not (Path(base_path) / template_path).exists():
|
152
|
+
raise FileNotFoundError(f"File not found: {template_path}")
|
153
|
+
if not isinstance(target_path, (Path, str)) or target_path == '':
|
154
|
+
raise ValueError(f"Invalid target_path: {target_path}")
|
155
|
+
if not isinstance(target_path, (Path, str)):
|
156
|
+
raise ValueError(f"Invalid target_path: {target_path}")
|
157
|
+
if Path(target_path).suffix != '.py':
|
158
|
+
raise ValueError(
|
159
|
+
f"Invalid target_path: {target_path}. Only .py file is supported"
|
160
|
+
)
|
161
|
+
else:
|
162
|
+
raise ValueError(f"Invalid workflow: {workflow}")
|
163
|
+
|
164
|
+
if not isinstance(mapping, dict):
|
165
|
+
raise ValueError(f"Invalid mapping: {mapping}")
|
166
|
+
|
167
|
+
for key, value in mapping.items():
|
168
|
+
if not isinstance(key, str):
|
169
|
+
raise ValueError(
|
170
|
+
f"Invalid key: {key}, should be str type and valid identifier")
|
171
|
+
if not key.isidentifier():
|
172
|
+
raise ValueError(f"Invalid key: {key}, should be identifier")
|
173
|
+
try:
|
174
|
+
pickle.dumps(value)
|
175
|
+
except Exception as e:
|
176
|
+
raise ValueError(
|
177
|
+
f"Invalid value: {key}: {value}, should be pickleable") from e
|
178
|
+
return
|
179
|
+
|
180
|
+
|
181
|
+
def verify_depends(module: WorkflowType, base_path):
|
182
|
+
if not hasattr(module, 'depends'):
|
183
|
+
return
|
184
|
+
|
185
|
+
deps = []
|
186
|
+
|
187
|
+
if callable(module.depends):
|
188
|
+
if not can_call_without_args(module.depends):
|
189
|
+
raise AttributeError(
|
190
|
+
f"Workflow {module.__file__} 'depends' function should not have any parameters"
|
191
|
+
)
|
192
|
+
deps = list(module.depends())
|
193
|
+
elif isinstance(module.depends, (list, tuple)):
|
194
|
+
deps = module.depends
|
195
|
+
else:
|
196
|
+
raise AttributeError(
|
197
|
+
f"Workflow {module.__file__} 'depends' should be a callable or a list"
|
198
|
+
)
|
199
|
+
for workflow in deps:
|
200
|
+
verify_dependence_key(workflow, base_path)
|
201
|
+
|
202
|
+
|
203
|
+
def verify_entries(module: WorkflowType, base_path):
|
204
|
+
if not hasattr(module, 'entries'):
|
205
|
+
return
|
206
|
+
|
207
|
+
deps = []
|
208
|
+
|
209
|
+
if callable(module.entries):
|
210
|
+
if not can_call_without_args(module.entries):
|
211
|
+
raise AttributeError(
|
212
|
+
f"Workflow {module.__file__} 'entries' function should not have any parameters"
|
213
|
+
)
|
214
|
+
deps = list(module.entries())
|
215
|
+
elif isinstance(module.entries, (list, tuple)):
|
216
|
+
deps = module.entries
|
217
|
+
else:
|
218
|
+
raise AttributeError(
|
219
|
+
f"Workflow {module.__file__} 'entries' should be a callable or a list"
|
220
|
+
)
|
221
|
+
for workflow in deps:
|
222
|
+
verify_dependence_key(workflow, base_path)
|
223
|
+
|
224
|
+
|
225
|
+
def is_workflow(module: ModuleType) -> bool:
|
226
|
+
try:
|
227
|
+
verify_calibrate_method(module)
|
228
|
+
return True
|
229
|
+
except AttributeError:
|
230
|
+
return False
|
231
|
+
|
232
|
+
|
233
|
+
def is_template(path: str | Path) -> bool:
|
234
|
+
path = Path(path)
|
235
|
+
if path.name == 'template.py':
|
236
|
+
return True
|
237
|
+
if path.name.endswith('_template.py'):
|
238
|
+
return True
|
239
|
+
if 'templates' in path.parts:
|
240
|
+
return True
|
241
|
+
return False
|
242
|
+
|
243
|
+
|
244
|
+
def find_unreferenced_workflows(path: str) -> list[str]:
|
245
|
+
root = Path(path).resolve()
|
246
|
+
workflows = []
|
247
|
+
workflow_paths: set[str] = set()
|
248
|
+
|
249
|
+
# Collect all workflow modules
|
250
|
+
for file_path in root.rglob("*.py"):
|
251
|
+
if file_path.name == "__init__.py":
|
252
|
+
continue
|
253
|
+
if is_template(file_path):
|
254
|
+
continue
|
255
|
+
try:
|
256
|
+
rel_path = file_path.relative_to(root)
|
257
|
+
except ValueError:
|
258
|
+
continue
|
259
|
+
|
260
|
+
module = load_workflow_from_file(str(rel_path), root)
|
261
|
+
|
262
|
+
if is_workflow(module):
|
263
|
+
rel_str = str(rel_path)
|
264
|
+
workflows.append(rel_str)
|
265
|
+
workflow_paths.add(rel_str)
|
266
|
+
|
267
|
+
dependencies: set[str] = set()
|
268
|
+
|
269
|
+
# Check dependencies for each workflow module
|
270
|
+
for rel_str in workflows:
|
271
|
+
module = load_workflow_from_file(rel_str, root)
|
272
|
+
|
273
|
+
depends_func = getattr(module, "depends", None)
|
274
|
+
if depends_func and callable(depends_func):
|
275
|
+
if not can_call_without_args(depends_func):
|
276
|
+
warnings.warn(
|
277
|
+
f"Skipping depends() in {rel_str} as it requires arguments"
|
278
|
+
)
|
279
|
+
continue
|
280
|
+
try:
|
281
|
+
depends_list = [
|
282
|
+
n.__workflow_id__ for n in get_dependents(module, root)
|
283
|
+
]
|
284
|
+
except Exception as e:
|
285
|
+
warnings.warn(f"Error calling depends() in {rel_str}: {e}")
|
286
|
+
continue
|
287
|
+
|
288
|
+
if not isinstance(depends_list, list) or not all(
|
289
|
+
isinstance(item, str) for item in depends_list):
|
290
|
+
warnings.warn(
|
291
|
+
f"depends() in {rel_str} did not return a list of strings")
|
292
|
+
continue
|
293
|
+
|
294
|
+
for dep in depends_list:
|
295
|
+
dep_full = (root / dep).resolve()
|
296
|
+
try:
|
297
|
+
dep_rel = dep_full.relative_to(root)
|
298
|
+
except ValueError:
|
299
|
+
continue
|
300
|
+
dep_rel_str = str(dep_rel)
|
301
|
+
if dep_rel_str in workflow_paths:
|
302
|
+
dependencies.add(dep_rel_str)
|
303
|
+
|
304
|
+
# Determine unreferenced workflows
|
305
|
+
unreferenced = [wp for wp in workflows if wp not in dependencies]
|
306
|
+
return unreferenced
|
307
|
+
|
308
|
+
|
309
|
+
def load_workflow_from_file(file_name: str,
|
310
|
+
base_path: str | Path,
|
311
|
+
package='workflows',
|
312
|
+
veryfy_source_code: bool = True) -> WorkflowType:
|
313
|
+
base_path = Path(base_path)
|
314
|
+
path = Path(file_name)
|
315
|
+
if not (base_path / path).exists():
|
316
|
+
raise FileNotFoundError(f"File not found: {base_path / path}")
|
317
|
+
module_name = f"{package}.{'.'.join([*path.parts[:-1], path.stem])}"
|
318
|
+
spec = spec_from_file_location(module_name, base_path / path)
|
319
|
+
module = module_from_spec(spec)
|
320
|
+
spec.loader.exec_module(module)
|
321
|
+
module.__mtime__ = (base_path / path).stat().st_mtime
|
322
|
+
source_code = (base_path / path).read_text()
|
323
|
+
module.__source__ = source_code
|
324
|
+
|
325
|
+
if hasattr(module, 'entries'):
|
326
|
+
if veryfy_source_code:
|
327
|
+
verify_entries(module, base_path)
|
328
|
+
return module
|
329
|
+
|
330
|
+
if not hasattr(module, '__timeout__'):
|
331
|
+
module.__timeout__ = None
|
332
|
+
|
333
|
+
if not hasattr(module, 'depends'):
|
334
|
+
module.depends = lambda: []
|
335
|
+
|
336
|
+
if veryfy_source_code:
|
337
|
+
verify_depends(module, base_path)
|
338
|
+
verify_calibrate_method(module)
|
339
|
+
verify_check_method(module)
|
340
|
+
|
341
|
+
return module
|
342
|
+
|
343
|
+
|
344
|
+
def load_workflow_from_source_code(workflow_id: str,
|
345
|
+
source_code: str,
|
346
|
+
package='workflows') -> WorkflowType:
|
347
|
+
workflow_path = Path(workflow_id)
|
348
|
+
module_name = f"{package}.{'.'.join([*workflow_path.parts[:-1], workflow_path.stem])}"
|
349
|
+
|
350
|
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.py').name
|
351
|
+
with open(temp_file, 'w') as f:
|
352
|
+
f.write(source_code)
|
353
|
+
|
354
|
+
atexit.register(lambda f=temp_file: Path(f).unlink(missing_ok=True))
|
355
|
+
|
356
|
+
spec = spec_from_file_location(module_name, temp_file)
|
357
|
+
module = module_from_spec(spec)
|
358
|
+
spec.loader.exec_module(module)
|
359
|
+
|
360
|
+
module.__source__ = source_code
|
361
|
+
module.__mtime__ = 0
|
362
|
+
module.__workflow_id__ = workflow_id
|
363
|
+
|
364
|
+
if not hasattr(module, '__timeout__'):
|
365
|
+
module.__timeout__ = None
|
366
|
+
|
367
|
+
if not hasattr(module, 'depends'):
|
368
|
+
module.depends = lambda: []
|
369
|
+
|
370
|
+
return module
|
371
|
+
|
372
|
+
|
373
|
+
def _generate_target_file_path(template_path: str | Path, hash_str: str,
|
374
|
+
content: str, base_path: str | Path) -> Path:
|
375
|
+
path = Path(template_path)
|
376
|
+
if path.stem == 'template':
|
377
|
+
path = path.parent / f'tmp{hash_str}.py'
|
378
|
+
elif path.stem.endswith('_template'):
|
379
|
+
path = path.parent / path.stem.replace('_template',
|
380
|
+
f'_tmp{hash_str}.py')
|
381
|
+
else:
|
382
|
+
path = path.parent / f'{path.stem}_tmp{hash_str}.py'
|
383
|
+
|
384
|
+
if 'templates' in path.parts:
|
385
|
+
return Path(*['run' if p == 'templates' else p for p in path.parts])
|
386
|
+
else:
|
387
|
+
return Path('run') / path
|
388
|
+
|
389
|
+
|
390
|
+
def load_workflow_from_template(
|
391
|
+
template_path: str,
|
392
|
+
mapping: dict[str, str],
|
393
|
+
base_path: str | Path,
|
394
|
+
target_path: str | None = None,
|
395
|
+
package='workflows',
|
396
|
+
mtime: float = 0,
|
397
|
+
veryfy_source_code: bool = True) -> WorkflowType:
|
398
|
+
base_path = Path(base_path)
|
399
|
+
path = Path(template_path)
|
400
|
+
|
401
|
+
with open(base_path / path) as f:
|
402
|
+
template = f.read()
|
403
|
+
|
404
|
+
mtime = max((base_path / template_path).stat().st_mtime, mtime)
|
405
|
+
|
406
|
+
content, hash_str = inject_mapping(template, mapping, str(path))
|
407
|
+
|
408
|
+
if target_path is None:
|
409
|
+
path = _generate_target_file_path(template_path, hash_str, content,
|
410
|
+
base_path)
|
411
|
+
else:
|
412
|
+
path = target_path
|
413
|
+
|
414
|
+
file = base_path / path
|
415
|
+
if not file.exists() or (file.stat().st_mtime < mtime
|
416
|
+
and file.read_text() != content):
|
417
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
418
|
+
with open(file, 'w') as f:
|
419
|
+
f.write(content)
|
420
|
+
elif file.read_text() != content:
|
421
|
+
logger.warning(
|
422
|
+
f"`{file}` already exists and is different from the new one generated from template `{template_path}`"
|
423
|
+
)
|
424
|
+
|
425
|
+
module = load_workflow_from_file(str(path), base_path, package,
|
426
|
+
veryfy_source_code)
|
427
|
+
module.__mtime__ = max(mtime, module.__mtime__)
|
428
|
+
if module.__source__ == content:
|
429
|
+
module.__source__ = template, mapping, str(template_path)
|
430
|
+
|
431
|
+
return module
|
432
|
+
|
433
|
+
|
434
|
+
def load_workflow(workflow: str | tuple[str, dict],
|
435
|
+
base_path: str | Path,
|
436
|
+
package='workflows',
|
437
|
+
mtime: float = 0,
|
438
|
+
inject: dict | None = None,
|
439
|
+
veryfy_source_code: bool = True) -> WorkflowType:
|
440
|
+
if isinstance(workflow, tuple):
|
441
|
+
if len(workflow) == 2:
|
442
|
+
file_name, mapping = workflow
|
443
|
+
if inject is None:
|
444
|
+
w = load_workflow_from_template(file_name, mapping, base_path,
|
445
|
+
None, package, mtime,
|
446
|
+
veryfy_source_code)
|
447
|
+
else:
|
448
|
+
w = load_workflow_from_template(file_name, inject, base_path,
|
449
|
+
None, package, mtime,
|
450
|
+
veryfy_source_code)
|
451
|
+
elif len(workflow) == 3:
|
452
|
+
template_path, target_path, mapping = workflow
|
453
|
+
if inject is None:
|
454
|
+
w = load_workflow_from_template(template_path, mapping,
|
455
|
+
base_path, target_path,
|
456
|
+
package, mtime,
|
457
|
+
veryfy_source_code)
|
458
|
+
else:
|
459
|
+
w = load_workflow_from_template(template_path, inject,
|
460
|
+
base_path, target_path,
|
461
|
+
package, mtime,
|
462
|
+
veryfy_source_code)
|
463
|
+
else:
|
464
|
+
raise ValueError(f"Invalid workflow: {workflow}")
|
465
|
+
w.__workflow_id__ = str(Path(w.__file__).relative_to(base_path))
|
466
|
+
elif isinstance(workflow, str):
|
467
|
+
if workflow.startswith('cfg:'):
|
468
|
+
key = workflow[4:]
|
469
|
+
w = SetConfigWorkflow(key)
|
470
|
+
w.__workflow_id__ = workflow
|
471
|
+
else:
|
472
|
+
w = load_workflow_from_file(workflow, base_path, package,
|
473
|
+
veryfy_source_code)
|
474
|
+
w.__workflow_id__ = str(Path(w.__file__).relative_to(base_path))
|
475
|
+
else:
|
476
|
+
raise TypeError(f"Invalid workflow: {workflow}")
|
477
|
+
|
478
|
+
return w
|
479
|
+
|
480
|
+
|
481
|
+
def _load_workflow_list(workflow, lst, code_path, veryfy_source_code):
|
482
|
+
ret = []
|
483
|
+
for i, n in enumerate(lst):
|
484
|
+
try:
|
485
|
+
ret.append(
|
486
|
+
load_workflow(n,
|
487
|
+
code_path,
|
488
|
+
mtime=workflow.__mtime__,
|
489
|
+
veryfy_source_code=veryfy_source_code))
|
490
|
+
except TemplateKeyError as e:
|
491
|
+
msg, *_ = e.args
|
492
|
+
e.args = (
|
493
|
+
f"Workflow {workflow.__workflow_id__} entry {i}: {msg}", )
|
494
|
+
raise e
|
495
|
+
return ret
|
496
|
+
|
497
|
+
|
498
|
+
def get_dependents(workflow: WorkflowType, code_path: str | Path,
|
499
|
+
veryfy_source_code: bool) -> list[WorkflowType]:
|
500
|
+
if callable(getattr(workflow, 'depends', None)):
|
501
|
+
if not can_call_without_args(workflow.depends):
|
502
|
+
raise AttributeError(
|
503
|
+
f'Workflow {workflow.__workflow_id__} "depends" function should not have any parameters'
|
504
|
+
)
|
505
|
+
return _load_workflow_list(workflow, workflow.depends(), code_path,
|
506
|
+
veryfy_source_code)
|
507
|
+
elif isinstance(getattr(workflow, 'depends', None), (list, tuple)):
|
508
|
+
return _load_workflow_list(workflow, workflow.depends, code_path,
|
509
|
+
veryfy_source_code)
|
510
|
+
elif getattr(workflow, 'depends', None) is None:
|
511
|
+
return []
|
512
|
+
else:
|
513
|
+
raise AttributeError(
|
514
|
+
f'Workflow {workflow.__workflow_id__} "depends" should be a callable or a list'
|
515
|
+
)
|
516
|
+
|
517
|
+
|
518
|
+
def get_entries(workflow: WorkflowType, code_path: str | Path,
|
519
|
+
veryfy_source_code: bool) -> list[WorkflowType]:
|
520
|
+
if callable(getattr(workflow, 'entries', None)):
|
521
|
+
if not can_call_without_args(workflow.entries):
|
522
|
+
raise AttributeError(
|
523
|
+
f'Workflow {workflow.__workflow_id__} "entries" function should not have any parameters'
|
524
|
+
)
|
525
|
+
return _load_workflow_list(workflow, workflow.entries(), code_path,
|
526
|
+
veryfy_source_code)
|
527
|
+
elif isinstance(getattr(workflow, 'entries', None), (list, tuple)):
|
528
|
+
return _load_workflow_list(workflow, workflow.entries, code_path,
|
529
|
+
veryfy_source_code)
|
530
|
+
elif getattr(workflow, 'entries', None) is None:
|
531
|
+
return []
|
532
|
+
else:
|
533
|
+
raise AttributeError(
|
534
|
+
f'Workflow {workflow.__workflow_id__} "entries" should be a callable or a list'
|
535
|
+
)
|
536
|
+
|
537
|
+
|
538
|
+
def make_graph(workflow: WorkflowType,
|
539
|
+
graph: dict,
|
540
|
+
code_path: str | Path,
|
541
|
+
veryfy_source_code: bool = True):
|
542
|
+
if workflow.__workflow_id__ in graph:
|
543
|
+
return graph
|
544
|
+
graph[workflow.__workflow_id__] = []
|
545
|
+
|
546
|
+
if hasattr(workflow, 'entries'):
|
547
|
+
for w in get_entries(workflow, code_path, veryfy_source_code):
|
548
|
+
graph[workflow.__workflow_id__].append(w.__workflow_id__)
|
549
|
+
make_graph(w,
|
550
|
+
graph=graph,
|
551
|
+
code_path=code_path,
|
552
|
+
veryfy_source_code=veryfy_source_code)
|
553
|
+
elif hasattr(workflow, 'depends'):
|
554
|
+
for w in get_dependents(workflow, code_path, veryfy_source_code):
|
555
|
+
graph[workflow.__workflow_id__].append(w.__workflow_id__)
|
556
|
+
make_graph(w,
|
557
|
+
graph=graph,
|
558
|
+
code_path=code_path,
|
559
|
+
veryfy_source_code=veryfy_source_code)
|
560
|
+
if graph[workflow.__workflow_id__] == []:
|
561
|
+
del graph[workflow.__workflow_id__]
|
562
|
+
|
563
|
+
return graph
|