sier2 0.24__tar.gz → 0.29__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.
Potentially problematic release.
This version of sier2 might be problematic. Click here for more details.
- {sier2-0.24 → sier2-0.29}/PKG-INFO +9 -14
- {sier2-0.24 → sier2-0.29}/pyproject.toml +9 -6
- {sier2-0.24 → sier2-0.29}/src/sier2/__init__.py +1 -1
- {sier2-0.24 → sier2-0.29}/src/sier2/_block.py +66 -39
- {sier2-0.24 → sier2-0.29}/src/sier2/_dag.py +90 -28
- {sier2-0.24 → sier2-0.29}/src/sier2/_library.py +25 -8
- {sier2-0.24 → sier2-0.29}/src/sier2/panel/_panel.py +55 -27
- {sier2-0.24 → sier2-0.29}/LICENSE +0 -0
- {sier2-0.24 → sier2-0.29}/README.rst +0 -0
- {sier2-0.24 → sier2-0.29}/src/sier2/__main__.py +0 -0
- {sier2-0.24 → sier2-0.29}/src/sier2/_logger.py +0 -0
- {sier2-0.24 → sier2-0.29}/src/sier2/_util.py +0 -0
- {sier2-0.24 → sier2-0.29}/src/sier2/_version.py +0 -0
- {sier2-0.24 → sier2-0.29}/src/sier2/panel/__init__.py +0 -0
- {sier2-0.24 → sier2-0.29}/src/sier2/panel/_feedlogger.py +0 -0
- {sier2-0.24 → sier2-0.29}/src/sier2/panel/_panel_util.py +0 -0
|
@@ -1,23 +1,18 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: sier2
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29
|
|
4
4
|
Summary: Blocks of code that are executed in dags
|
|
5
|
-
Author:
|
|
6
|
-
Author-email: algol60@users.noreply.github.com
|
|
7
|
-
Requires-Python: >=3.11,<4.0
|
|
8
|
-
Classifier: Intended Audience :: Developers
|
|
9
|
-
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Author: Algol60
|
|
6
|
+
Author-email: algol60 <algol60@users.noreply.github.com>
|
|
13
7
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
8
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier:
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
16
13
|
Classifier: Topic :: Scientific/Engineering
|
|
17
14
|
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
-
|
|
19
|
-
Requires-Dist: panel (>=1.4.4)
|
|
20
|
-
Requires-Dist: param (>=2.1.0)
|
|
15
|
+
Project-URL: Homepage, https://github.com/algol60/sier2
|
|
21
16
|
Description-Content-Type: text/x-rst
|
|
22
17
|
|
|
23
18
|
Sier2
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
[
|
|
1
|
+
[project]
|
|
2
2
|
name = "sier2"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.29"
|
|
4
4
|
description = "Blocks of code that are executed in dags"
|
|
5
|
-
authors = [
|
|
5
|
+
authors = [
|
|
6
|
+
{name="Algol60", email="algol60 <algol60@users.noreply.github.com>"}
|
|
7
|
+
]
|
|
6
8
|
readme = "README.rst"
|
|
7
9
|
packages = [{include = "sier2", from = "src"}]
|
|
8
10
|
classifiers = [
|
|
@@ -16,7 +18,7 @@ classifiers = [
|
|
|
16
18
|
"Topic :: Software Development :: Libraries"
|
|
17
19
|
]
|
|
18
20
|
|
|
19
|
-
[
|
|
21
|
+
[dependencies]
|
|
20
22
|
python = "^3.11"
|
|
21
23
|
|
|
22
24
|
holoviews = ">=1.19.0"
|
|
@@ -26,7 +28,8 @@ param = ">=2.1.0"
|
|
|
26
28
|
[[tool.mypy.overrides]]
|
|
27
29
|
module = [
|
|
28
30
|
"holoviews",
|
|
29
|
-
"param"
|
|
31
|
+
"param",
|
|
32
|
+
"networkx",
|
|
30
33
|
]
|
|
31
34
|
ignore_missing_imports = true
|
|
32
35
|
|
|
@@ -34,5 +37,5 @@ ignore_missing_imports = true
|
|
|
34
37
|
Homepage = "https://github.com/algol60/sier2"
|
|
35
38
|
|
|
36
39
|
[build-system]
|
|
37
|
-
requires = ["poetry-core"]
|
|
40
|
+
requires = ["poetry-core>=2.1.1"]
|
|
38
41
|
build-backend = "poetry.core.masonry.api"
|
|
@@ -27,6 +27,19 @@ class BlockState(StrEnum):
|
|
|
27
27
|
INTERRUPTED = 'INTERRUPTED'
|
|
28
28
|
ERROR = 'ERROR'
|
|
29
29
|
|
|
30
|
+
_PAUSE_EXECUTION_DOC = '''If True, a block executes in two steps.
|
|
31
|
+
|
|
32
|
+
When the block is executed by a dag, the dag first sets the input
|
|
33
|
+
params, then calls ``prepare()``. Execution of the dag then stops.
|
|
34
|
+
|
|
35
|
+
The dag is then restarted using ``dag.execute_after_input(input_block)``.
|
|
36
|
+
(An input block must be specified because it is not required that the
|
|
37
|
+
same input block be used immediately.) This causes the block's
|
|
38
|
+
``execute()`` method to be called without resetting the input params.
|
|
39
|
+
|
|
40
|
+
Dag execution then continues as normal.
|
|
41
|
+
'''
|
|
42
|
+
|
|
30
43
|
class Block(param.Parameterized):
|
|
31
44
|
"""The base class for blocks.
|
|
32
45
|
|
|
@@ -49,16 +62,19 @@ class Block(param.Parameterized):
|
|
|
49
62
|
print(f'New value is {self.value_in}')
|
|
50
63
|
"""
|
|
51
64
|
|
|
65
|
+
block_pause_execution = param.Boolean(default=False, label='Pause execution', doc=_PAUSE_EXECUTION_DOC)
|
|
66
|
+
|
|
52
67
|
_block_state = param.String(default=BlockState.READY)
|
|
53
68
|
|
|
54
69
|
SIER2_KEY = '_sier2__key'
|
|
55
70
|
|
|
56
|
-
def __init__(self, *args, continue_label='Continue', **kwargs):
|
|
71
|
+
def __init__(self, *args, block_pause_execution=False, continue_label='Continue', **kwargs):
|
|
57
72
|
super().__init__(*args, **kwargs)
|
|
58
73
|
|
|
59
74
|
if not self.__doc__:
|
|
60
75
|
raise BlockError(f'Class {self.__class__} must have a docstring')
|
|
61
76
|
|
|
77
|
+
self.block_pause_execution = block_pause_execution
|
|
62
78
|
self.continue_label = continue_label
|
|
63
79
|
# self._block_state = BlockState.READY
|
|
64
80
|
self.logger = _logger.get_logger(self.name)
|
|
@@ -77,7 +93,7 @@ class Block(param.Parameterized):
|
|
|
77
93
|
|
|
78
94
|
# self._block_context = _EmptyContext()
|
|
79
95
|
|
|
80
|
-
self._progress = None
|
|
96
|
+
# self._progress = None
|
|
81
97
|
|
|
82
98
|
@classmethod
|
|
83
99
|
def block_key(cls):
|
|
@@ -95,6 +111,17 @@ class Block(param.Parameterized):
|
|
|
95
111
|
|
|
96
112
|
return f'{im.__name__}.{cls.__qualname__}'
|
|
97
113
|
|
|
114
|
+
def prepare(self):
|
|
115
|
+
"""If blockpause_execution is True, called by a dag before calling ``execute()```.
|
|
116
|
+
|
|
117
|
+
This gives the block author an opportunity to validate the
|
|
118
|
+
input params and set up a user inteface.
|
|
119
|
+
|
|
120
|
+
After the dag restarts on this block, ``execute()`` will be called.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
pass
|
|
124
|
+
|
|
98
125
|
def execute(self, *_, **__):
|
|
99
126
|
"""This method is called when one or more of the input parameters causes an event.
|
|
100
127
|
|
|
@@ -113,24 +140,24 @@ class Block(param.Parameterized):
|
|
|
113
140
|
# print(f'** EXECUTE {self.__class__=}')
|
|
114
141
|
pass
|
|
115
142
|
|
|
116
|
-
def __panel__(self):
|
|
117
|
-
|
|
143
|
+
# def __panel__(self):
|
|
144
|
+
# """A default Panel component.
|
|
118
145
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
# When run in a Panel context, a block will typically implement
|
|
147
|
+
# its own __panel__() method. If it doesn't, this method will be
|
|
148
|
+
# used as a default. When a block without a __panel__() is wrapped
|
|
149
|
+
# in a Card, self.progress will be assigned a pn.indicators.Progress()
|
|
150
|
+
# widget which is returned here. The Panel context will make it active
|
|
151
|
+
# before executing the block, and non-active after executing the block.
|
|
152
|
+
# (Why not have a default Progress()? Because we don't want any
|
|
153
|
+
# Panel-related code in the core implementation.)
|
|
127
154
|
|
|
128
|
-
|
|
155
|
+
# If the block implements __panel__(), this will obviously be overridden.
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
# When run in non-Panel context, this will remain unused.
|
|
158
|
+
# """
|
|
132
159
|
|
|
133
|
-
|
|
160
|
+
# return self._progress
|
|
134
161
|
|
|
135
162
|
def __call__(self, **kwargs) -> dict[str, Any]:
|
|
136
163
|
"""Allow a block to be called directly."""
|
|
@@ -150,45 +177,45 @@ class Block(param.Parameterized):
|
|
|
150
177
|
|
|
151
178
|
return result
|
|
152
179
|
|
|
153
|
-
class InputBlock(Block):
|
|
154
|
-
|
|
180
|
+
# class InputBlock(Block):
|
|
181
|
+
# """A ``Block`` that accepts user input.
|
|
155
182
|
|
|
156
|
-
|
|
183
|
+
# An ``InputBlock`` executes in two steps().
|
|
157
184
|
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
# When the block is executed by a dag, the dag first sets the input
|
|
186
|
+
# params, then calls ``prepare()``. Execution of the dag then stops.
|
|
160
187
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
# The dag is then restarted using ``dag.execute_after_input(input_block)``.
|
|
189
|
+
# (An input block must be specified because it is not required that the
|
|
190
|
+
# same input block be used immediately.) This causes the block's
|
|
191
|
+
# ``execute()`` method to be called without resetting the input params.
|
|
165
192
|
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
# Dag execution then continues as normal.
|
|
194
|
+
# """
|
|
168
195
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
196
|
+
# def __init__(self, *args, continue_label='Continue', **kwargs):
|
|
197
|
+
# super().__init__(*args, continue_label=continue_label, **kwargs)
|
|
198
|
+
# self._block_state = BlockState.INPUT
|
|
172
199
|
|
|
173
|
-
|
|
174
|
-
|
|
200
|
+
# def prepare(self):
|
|
201
|
+
# """Called by a dag before calling ``execute()```.
|
|
175
202
|
|
|
176
|
-
|
|
177
|
-
|
|
203
|
+
# This gives the block author an opportunity to validate the
|
|
204
|
+
# input params and set up a user inteface.
|
|
178
205
|
|
|
179
|
-
|
|
180
|
-
|
|
206
|
+
# After the dag restarts on this block, ``execute()`` will be called.
|
|
207
|
+
# """
|
|
181
208
|
|
|
182
|
-
|
|
209
|
+
# pass
|
|
183
210
|
|
|
184
211
|
class BlockValidateError(BlockError):
|
|
185
|
-
"""Raised if ``
|
|
212
|
+
"""Raised if ``Block.prepare()`` or ``Block.execute()`` determines that input data is invalid.
|
|
186
213
|
|
|
187
214
|
If this exception is raised, it will be caught by the executing dag.
|
|
188
215
|
The dag will not set its stop flag, no stacktrace will be displayed,
|
|
189
216
|
and the error message will be displayed.
|
|
190
217
|
"""
|
|
191
218
|
|
|
192
|
-
def __init__(self, block_name: str, error: str):
|
|
219
|
+
def __init__(self, *, block_name: str, error: str):
|
|
193
220
|
super().__init__(error)
|
|
194
221
|
self.block_name = block_name
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from ._block import Block,
|
|
1
|
+
from ._block import Block, BlockError, BlockValidateError, BlockState
|
|
2
2
|
from dataclasses import dataclass, field #, KW_ONLY, field
|
|
3
3
|
from collections import defaultdict, deque
|
|
4
4
|
import holoviews as hv
|
|
5
|
+
from importlib.metadata import entry_points
|
|
5
6
|
import threading
|
|
6
7
|
import sys
|
|
7
8
|
from typing import Any
|
|
@@ -68,7 +69,7 @@ class _BlockContext:
|
|
|
68
69
|
|
|
69
70
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
70
71
|
if exc_type is None:
|
|
71
|
-
self.block._block_state = BlockState.WAITING if
|
|
72
|
+
self.block._block_state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
|
|
72
73
|
elif exc_type is KeyboardInterrupt:
|
|
73
74
|
self.block_state._block_state = BlockState.INTERRUPTED
|
|
74
75
|
if not self.dag._is_pyodide:
|
|
@@ -116,16 +117,48 @@ class _Stopper:
|
|
|
116
117
|
def __repr__(self):
|
|
117
118
|
return f'stopped={self.is_stopped}'
|
|
118
119
|
|
|
120
|
+
def _find_logging():
|
|
121
|
+
PLUGIN_GROUP = 'sier2.logging'
|
|
122
|
+
library = entry_points(group=PLUGIN_GROUP)
|
|
123
|
+
if (liblen:=len(library))==0:
|
|
124
|
+
# There is no logging plugin, so return a dummy.
|
|
125
|
+
#
|
|
126
|
+
return lambda f, *args, **kwargs: f
|
|
127
|
+
elif liblen>1:
|
|
128
|
+
raise BlockError(f'More than one plugin for {PLUGIN_GROUP}')
|
|
129
|
+
|
|
130
|
+
ep = next(iter(library))
|
|
131
|
+
try:
|
|
132
|
+
logging_func = ep.load()
|
|
133
|
+
|
|
134
|
+
return logging_func
|
|
135
|
+
except AttributeError as e:
|
|
136
|
+
e.add_note(f'While attempting to load logging function {ep.value}')
|
|
137
|
+
raise BlockError(e)
|
|
138
|
+
|
|
139
|
+
# A marker from Dag.execute_after_input() to tell Dag.execute()
|
|
140
|
+
# that this a restart.
|
|
141
|
+
#
|
|
142
|
+
_RESTART = ':restart:'
|
|
143
|
+
|
|
119
144
|
class Dag:
|
|
120
145
|
"""A directed acyclic graph of blocks."""
|
|
121
146
|
|
|
122
|
-
def __init__(self, *, site: str='Block', title: str, doc: str):
|
|
147
|
+
def __init__(self, *, site: str='Block', title: str, doc: str, author: dict[str, str]=None):
|
|
123
148
|
self._block_pairs: list[tuple[Block, Block]] = []
|
|
124
|
-
|
|
149
|
+
|
|
125
150
|
self.site = site
|
|
126
151
|
self.title = title
|
|
127
152
|
self.doc = doc
|
|
128
153
|
|
|
154
|
+
if author is not None:
|
|
155
|
+
if 'name' in author and 'email' in author:
|
|
156
|
+
self.author = {'name': author['name', 'email': author: 'email']}
|
|
157
|
+
else:
|
|
158
|
+
raise ValueError('Author must contain name and email keys')
|
|
159
|
+
else:
|
|
160
|
+
self.author = None
|
|
161
|
+
|
|
129
162
|
if not self._is_pyodide:
|
|
130
163
|
self._stopper = _Stopper()
|
|
131
164
|
|
|
@@ -138,6 +171,10 @@ class Dag:
|
|
|
138
171
|
#
|
|
139
172
|
self._block_context = _BlockContext
|
|
140
173
|
|
|
174
|
+
# Set up the logging hook.
|
|
175
|
+
#
|
|
176
|
+
self.logging = _find_logging()
|
|
177
|
+
|
|
141
178
|
@property
|
|
142
179
|
def _is_pyodide(self) -> bool:
|
|
143
180
|
return '_pyodide' in sys.modules
|
|
@@ -241,7 +278,7 @@ class Dag:
|
|
|
241
278
|
item.values[inp] = new
|
|
242
279
|
self._block_queue.append(item)
|
|
243
280
|
|
|
244
|
-
def execute_after_input(self, block:
|
|
281
|
+
def execute_after_input(self, block: Block, *, dag_logger=None):
|
|
245
282
|
"""Execute the dag after running ``prepare()`` in an input block.
|
|
246
283
|
|
|
247
284
|
After prepare() executes, and the user has possibly
|
|
@@ -253,23 +290,23 @@ class Dag:
|
|
|
253
290
|
|
|
254
291
|
Parameters
|
|
255
292
|
----------
|
|
256
|
-
block:
|
|
293
|
+
block: Block
|
|
257
294
|
The block to restart the dag at.
|
|
258
295
|
dag_logger:
|
|
259
296
|
A logger adapter that will accept log messages.
|
|
260
297
|
"""
|
|
261
298
|
|
|
262
|
-
if not
|
|
263
|
-
raise BlockError(f'A dag can only restart
|
|
299
|
+
if not block.block_pause_execution:
|
|
300
|
+
raise BlockError(f'A dag can only restart a paused Block, not {block.name}')
|
|
264
301
|
|
|
265
|
-
# Prime the block queue, using
|
|
302
|
+
# Prime the block queue, using _RESTART
|
|
266
303
|
# to indicate that this is a restart, and Block.execute()
|
|
267
304
|
# must be called.
|
|
268
305
|
#
|
|
269
|
-
self._block_queue.appendleft(_InputValues(block, {}))
|
|
306
|
+
self._block_queue.appendleft(_InputValues(block, {_RESTART: True}))
|
|
270
307
|
self.execute(dag_logger=dag_logger)
|
|
271
308
|
|
|
272
|
-
def execute(self, *, dag_logger=None):
|
|
309
|
+
def execute(self, *, dag_logger=None) -> Block|None:
|
|
273
310
|
"""Execute the dag.
|
|
274
311
|
|
|
275
312
|
The dag is executed by iterating through the block event queue
|
|
@@ -277,23 +314,36 @@ class Dag:
|
|
|
277
314
|
update the destination block's input parameters and call
|
|
278
315
|
that block's execute() method.
|
|
279
316
|
|
|
280
|
-
If the current destination block is
|
|
317
|
+
If the current destination block's ``block_pause_execution` is True,
|
|
281
318
|
the loop will call ``block.prepare()` instead of ``block.execute()``,
|
|
282
|
-
then stop
|
|
283
|
-
``dag.execute_after_input()
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
the dag
|
|
287
|
-
output param before the dag's execute() is called
|
|
288
|
-
|
|
289
|
-
|
|
319
|
+
then stop; execute() will return the block that is puased on.
|
|
320
|
+
The dag can then be restarted with ``dag.execute_after_input()``,
|
|
321
|
+
using the paused block as the parameter.
|
|
322
|
+
|
|
323
|
+
To start the dag, either:
|
|
324
|
+
- there must be something in the event queue - the dag must be "primed". A block must have updated at least one output param before the dag's execute() is called;
|
|
325
|
+
- the first block in the dag must be an input block (block_pause_execution=True).
|
|
326
|
+
|
|
327
|
+
Calling ``dag.execute()`` will then execute the dag starting with the relevant block.
|
|
290
328
|
"""
|
|
291
329
|
|
|
330
|
+
if not self._block_queue:
|
|
331
|
+
# If there aren't any blocks on the queue, find the first block in the dag.
|
|
332
|
+
# If this block is an input block, put it on the queue.
|
|
333
|
+
#
|
|
334
|
+
sorted_blocks = self.get_sorted()
|
|
335
|
+
if sorted_blocks:
|
|
336
|
+
first = sorted_blocks[0]
|
|
337
|
+
if first.block_pause_execution:
|
|
338
|
+
self._block_queue.appendleft(_InputValues(first, {}))
|
|
339
|
+
|
|
292
340
|
if not self._block_queue:
|
|
293
341
|
# Attempting to execute a dag with no updates is probably a mistake.
|
|
294
342
|
#
|
|
295
343
|
raise BlockError('Nothing to execute')
|
|
296
344
|
|
|
345
|
+
self.logging(None, sier2_dag_=self)
|
|
346
|
+
|
|
297
347
|
can_execute = True
|
|
298
348
|
while self._block_queue:
|
|
299
349
|
# print(len(self._block_queue), self._block_queue)
|
|
@@ -305,6 +355,7 @@ class Dag:
|
|
|
305
355
|
can_execute = False
|
|
306
356
|
|
|
307
357
|
item = self._block_queue.popleft()
|
|
358
|
+
is_restart = item.values.pop(_RESTART, False)
|
|
308
359
|
try:
|
|
309
360
|
item.dst.param.update(item.values)
|
|
310
361
|
except ValueError as e:
|
|
@@ -318,18 +369,27 @@ class Dag:
|
|
|
318
369
|
# unless this is after the user has selected the "Continue"
|
|
319
370
|
# button.
|
|
320
371
|
#
|
|
321
|
-
is_input_block =
|
|
372
|
+
is_input_block = item.dst.block_pause_execution
|
|
322
373
|
if can_execute:
|
|
323
374
|
with self._block_context(block=item.dst, dag=self, dag_logger=dag_logger) as g:
|
|
375
|
+
|
|
376
|
+
logging_params = {
|
|
377
|
+
'sier2_dag_': self,
|
|
378
|
+
'sier2_block_': f'{item.dst}'
|
|
379
|
+
}
|
|
380
|
+
|
|
324
381
|
# If this is an input block, and there are input
|
|
325
382
|
# values, call prepare() if it exists.
|
|
326
383
|
#
|
|
327
|
-
if is_input_block and item.values:
|
|
328
|
-
g.prepare()
|
|
384
|
+
if is_input_block and not is_restart:# and item.values:
|
|
385
|
+
self.logging(g.prepare, **logging_params)()
|
|
329
386
|
else:
|
|
330
|
-
g.execute()
|
|
387
|
+
self.logging(g.execute, **logging_params)()
|
|
331
388
|
|
|
332
|
-
|
|
389
|
+
# print(f'{is_input_block=}')
|
|
390
|
+
# print(f'{is_restart=}')
|
|
391
|
+
# print(f'{item.values=}')
|
|
392
|
+
if is_input_block and not is_restart:# and item.values:
|
|
333
393
|
# If the current destination block requires user input,
|
|
334
394
|
# stop executing the dag immediately, because we don't
|
|
335
395
|
# want to be setting the input params of further blocks
|
|
@@ -338,7 +398,9 @@ class Dag:
|
|
|
338
398
|
# This possibly leaves items on the queue, which will be
|
|
339
399
|
# executed on the next call to execute().
|
|
340
400
|
#
|
|
341
|
-
|
|
401
|
+
return item.dst
|
|
402
|
+
|
|
403
|
+
return None
|
|
342
404
|
|
|
343
405
|
def disconnect(self, g: Block) -> None:
|
|
344
406
|
"""Disconnect block g from other blocks.
|
|
@@ -384,7 +446,7 @@ class Dag:
|
|
|
384
446
|
|
|
385
447
|
return None
|
|
386
448
|
|
|
387
|
-
def get_sorted(self):
|
|
449
|
+
def get_sorted(self) -> list[Block]:
|
|
388
450
|
"""Return the blocks in this dag in topological order.
|
|
389
451
|
|
|
390
452
|
This is useful for arranging the blocks in a GUI, for example.
|
|
@@ -623,7 +685,7 @@ def _has_cycle(block_pairs: list[tuple[Block, Block]]):
|
|
|
623
685
|
|
|
624
686
|
return len(remaining)>0
|
|
625
687
|
|
|
626
|
-
def _get_sorted(block_pairs: list[tuple[Block, Block]]):
|
|
688
|
+
def _get_sorted(block_pairs: list[tuple[Block, Block]]) -> list[Block]:
|
|
627
689
|
ordered, remaining = topological_sort(block_pairs)
|
|
628
690
|
|
|
629
691
|
if remaining:
|
|
@@ -24,6 +24,29 @@ def docstring(func) -> str:
|
|
|
24
24
|
|
|
25
25
|
return doc.split('\n')[0].strip()
|
|
26
26
|
|
|
27
|
+
def _import_item(key):
|
|
28
|
+
"""Look up an object by key.
|
|
29
|
+
|
|
30
|
+
The returned object may be a class (if a Block key) or a function (if a dag key).
|
|
31
|
+
|
|
32
|
+
See the Entry points specification at
|
|
33
|
+
https://packaging.python.org/en/latest/specifications/entry-points/#entry-points.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
modname, qualname_separator, qualname = key.partition(':')
|
|
37
|
+
try:
|
|
38
|
+
obj = importlib.import_module(modname)
|
|
39
|
+
if qualname_separator:
|
|
40
|
+
for attr in qualname.split('.'):
|
|
41
|
+
obj = getattr(obj, attr)
|
|
42
|
+
|
|
43
|
+
return obj
|
|
44
|
+
except ModuleNotFoundError as e:
|
|
45
|
+
msg = str(e)
|
|
46
|
+
if not qualname_separator:
|
|
47
|
+
msg = f'{msg}. Is there a \':\' missing?'
|
|
48
|
+
raise BlockError(msg)
|
|
49
|
+
|
|
27
50
|
def _find_blocks():
|
|
28
51
|
yield from _find('blocks')
|
|
29
52
|
|
|
@@ -143,8 +166,6 @@ class Library:
|
|
|
143
166
|
if not issubclass(block_class, Block):
|
|
144
167
|
print(f'{key} is not a Block')
|
|
145
168
|
|
|
146
|
-
# if not key:
|
|
147
|
-
# key = block_class.block_key()
|
|
148
169
|
key_ = key if key else block_class.block_key()
|
|
149
170
|
|
|
150
171
|
if key_ in _block_library:
|
|
@@ -161,9 +182,7 @@ class Library:
|
|
|
161
182
|
raise BlockError(f'Block name {key} is not in the library')
|
|
162
183
|
|
|
163
184
|
if _block_library[key] is None:
|
|
164
|
-
|
|
165
|
-
m = importlib.import_module(key[:ix])
|
|
166
|
-
cls = getattr(m, key[ix+1:])
|
|
185
|
+
cls = _import_item(key)
|
|
167
186
|
if not issubclass(cls, Block):
|
|
168
187
|
raise BlockError(f'{key} is not a block')
|
|
169
188
|
|
|
@@ -188,9 +207,7 @@ class Library:
|
|
|
188
207
|
raise BlockError(f'Dag name {key} is not in the library')
|
|
189
208
|
|
|
190
209
|
if key in _dag_library:
|
|
191
|
-
|
|
192
|
-
m = importlib.import_module(key[:ix])
|
|
193
|
-
func = getattr(m, key[ix+1:])
|
|
210
|
+
func = _import_item(key)
|
|
194
211
|
dag = func()
|
|
195
212
|
if not isinstance(dag, Dag):
|
|
196
213
|
raise BlockError(f'{key} is not a dag')
|
|
@@ -4,12 +4,15 @@ import html
|
|
|
4
4
|
import panel as pn
|
|
5
5
|
import sys
|
|
6
6
|
import threading
|
|
7
|
+
from typing import Callable
|
|
7
8
|
|
|
8
|
-
from sier2 import Block,
|
|
9
|
+
from sier2 import Block, BlockValidateError, BlockState, Dag, BlockError
|
|
9
10
|
from .._dag import _InputValues
|
|
10
11
|
from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
|
|
11
12
|
from ._panel_util import _get_state_color, dag_doc
|
|
12
13
|
|
|
14
|
+
NTHREADS = 2
|
|
15
|
+
|
|
13
16
|
# From https://tabler.io/icons/icon/info-circle
|
|
14
17
|
#
|
|
15
18
|
INFO_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -25,17 +28,17 @@ if '_pyodide' in sys.modules:
|
|
|
25
28
|
# Specifying one thread for panel for some reason tries to start one, so we need to rely on the default.
|
|
26
29
|
#
|
|
27
30
|
pn.extension(
|
|
28
|
-
'floatpanel',
|
|
29
|
-
inline=True,
|
|
30
|
-
loading_spinner='bar',
|
|
31
|
+
'floatpanel',
|
|
32
|
+
inline=True,
|
|
33
|
+
loading_spinner='bar',
|
|
31
34
|
notifications=True,
|
|
32
35
|
)
|
|
33
36
|
else:
|
|
34
37
|
pn.extension(
|
|
35
|
-
'floatpanel',
|
|
36
|
-
inline=True,
|
|
37
|
-
nthreads=
|
|
38
|
-
loading_spinner='bar',
|
|
38
|
+
'floatpanel',
|
|
39
|
+
inline=True,
|
|
40
|
+
nthreads=NTHREADS,
|
|
41
|
+
loading_spinner='bar',
|
|
39
42
|
notifications=True,
|
|
40
43
|
)
|
|
41
44
|
|
|
@@ -71,21 +74,19 @@ class _PanelContext:
|
|
|
71
74
|
block_logger = getBlockPanelLogger(self.block.name)
|
|
72
75
|
self.block.logger = block_logger
|
|
73
76
|
|
|
74
|
-
if self.block._progress:
|
|
75
|
-
|
|
77
|
+
# if self.block._progress:
|
|
78
|
+
# self.block._progress.active = True
|
|
76
79
|
|
|
77
80
|
return self.block
|
|
78
81
|
|
|
79
82
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
80
|
-
|
|
81
|
-
print('c')
|
|
82
83
|
delta = (datetime.now() - self.t0).total_seconds()
|
|
83
84
|
|
|
84
|
-
if self.block._progress:
|
|
85
|
-
|
|
85
|
+
# if self.block._progress:
|
|
86
|
+
# self.block._progress.active = False
|
|
86
87
|
|
|
87
88
|
if exc_type is None:
|
|
88
|
-
state = BlockState.WAITING if
|
|
89
|
+
state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
|
|
89
90
|
self.block._block_state = state
|
|
90
91
|
if self.dag_logger:
|
|
91
92
|
self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
|
|
@@ -136,7 +137,7 @@ def interrupt_thread(tid, exctype):
|
|
|
136
137
|
#
|
|
137
138
|
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
|
|
138
139
|
raise SystemError('PyThreadState_SetAsyncExc failed')
|
|
139
|
-
|
|
140
|
+
|
|
140
141
|
def _prepare_to_show(dag: Dag):
|
|
141
142
|
# Replace the default text-based context with the panel-based context.
|
|
142
143
|
#
|
|
@@ -247,6 +248,13 @@ def _show_dag(dag: Dag):
|
|
|
247
248
|
|
|
248
249
|
pn.state.on_session_destroyed(_quit)
|
|
249
250
|
|
|
251
|
+
# Execute the dag.
|
|
252
|
+
# Since this is a panel dag, we expect the first block to be an input nlock.
|
|
253
|
+
# This ensures that the first block's prepare() method is called.
|
|
254
|
+
# If the first block is not an input block, it must be primed, just like a plain dag.
|
|
255
|
+
#
|
|
256
|
+
dag.execute()
|
|
257
|
+
|
|
250
258
|
template.show(threaded=False)
|
|
251
259
|
|
|
252
260
|
def _serveable_dag(dag: Dag):
|
|
@@ -256,8 +264,25 @@ def _serveable_dag(dag: Dag):
|
|
|
256
264
|
|
|
257
265
|
pn.state.on_session_destroyed(_quit)
|
|
258
266
|
|
|
267
|
+
# Execute the dag.
|
|
268
|
+
# Since this is a panel dag, we expect the first block to be an input nlock.
|
|
269
|
+
# This ensures that the first block's prepare() method is called.
|
|
270
|
+
# If the first block is not an input block, it must be primed, just like a plain dag.
|
|
271
|
+
#
|
|
272
|
+
dag.execute()
|
|
273
|
+
|
|
259
274
|
template.servable()
|
|
260
275
|
|
|
276
|
+
def _default_panel(self) -> Callable[[Block], pn.Param]:
|
|
277
|
+
"""Provide a default __panel__() implementation for blocks that don't have one.
|
|
278
|
+
|
|
279
|
+
This default will display the in_ parameters.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
in_names = [name for name in self.param.values() if name.startswith('in_')]
|
|
283
|
+
|
|
284
|
+
return pn.Param(self, parameters=in_names, show_name=False)
|
|
285
|
+
|
|
261
286
|
class BlockCard(pn.Card):
|
|
262
287
|
"""A custom card to wrap around a block.
|
|
263
288
|
|
|
@@ -294,19 +319,22 @@ class BlockCard(pn.Card):
|
|
|
294
319
|
# inspect the class and display the param attributes.
|
|
295
320
|
# This is obviously not what we want.
|
|
296
321
|
#
|
|
297
|
-
#
|
|
298
|
-
# The Panel context manager will activate and deactivate it.
|
|
322
|
+
# We just want to display the in_ params.
|
|
299
323
|
#
|
|
300
324
|
has_panel = '__panel__' in w.__class__.__dict__
|
|
301
325
|
if not has_panel:
|
|
302
|
-
w._progress = pn.indicators.Progress(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
)
|
|
326
|
+
# w._progress = pn.indicators.Progress(
|
|
327
|
+
# name='Block progress',
|
|
328
|
+
# bar_color='primary',
|
|
329
|
+
# active=False,
|
|
330
|
+
# value=-1
|
|
331
|
+
# )
|
|
332
|
+
|
|
333
|
+
# Go go gadget descriptor protocol.
|
|
334
|
+
#
|
|
335
|
+
w.__panel__ = _default_panel.__get__(w)
|
|
308
336
|
|
|
309
|
-
if
|
|
337
|
+
if w.block_pause_execution:
|
|
310
338
|
# This is an input block, so add a 'Continue' button.
|
|
311
339
|
#
|
|
312
340
|
def on_continue(_event):
|
|
@@ -318,7 +346,7 @@ class BlockCard(pn.Card):
|
|
|
318
346
|
#
|
|
319
347
|
parent_template.main[0].loading = True
|
|
320
348
|
w.param.trigger(*w._block_out_params)
|
|
321
|
-
|
|
349
|
+
|
|
322
350
|
try:
|
|
323
351
|
if dag_logger:
|
|
324
352
|
dag_logger.info('', block_name=None, block_state=None)
|
|
@@ -343,7 +371,7 @@ class BlockCard(pn.Card):
|
|
|
343
371
|
parent_template.main[0].loading = False
|
|
344
372
|
c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
|
|
345
373
|
c_button.on_click(on_continue)
|
|
346
|
-
|
|
374
|
+
|
|
347
375
|
w_ = pn.Column(
|
|
348
376
|
w,
|
|
349
377
|
pn.Row(c_button, align='end'),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|