sier2 0.23__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.23 → sier2-0.29}/PKG-INFO +9 -14
- {sier2-0.23 → sier2-0.29}/pyproject.toml +9 -6
- {sier2-0.23 → sier2-0.29}/src/sier2/__init__.py +1 -1
- {sier2-0.23 → sier2-0.29}/src/sier2/_block.py +66 -39
- {sier2-0.23 → sier2-0.29}/src/sier2/_dag.py +111 -37
- {sier2-0.23 → sier2-0.29}/src/sier2/_library.py +25 -8
- {sier2-0.23 → sier2-0.29}/src/sier2/panel/_panel.py +70 -22
- {sier2-0.23 → sier2-0.29}/LICENSE +0 -0
- {sier2-0.23 → sier2-0.29}/README.rst +0 -0
- {sier2-0.23 → sier2-0.29}/src/sier2/__main__.py +0 -0
- {sier2-0.23 → sier2-0.29}/src/sier2/_logger.py +0 -0
- {sier2-0.23 → sier2-0.29}/src/sier2/_util.py +0 -0
- {sier2-0.23 → sier2-0.29}/src/sier2/_version.py +0 -0
- {sier2-0.23 → sier2-0.29}/src/sier2/panel/__init__.py +0 -0
- {sier2-0.23 → sier2-0.29}/src/sier2/panel/_feedlogger.py +0 -0
- {sier2-0.23 → 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,8 +1,10 @@
|
|
|
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
|
|
7
|
+
import sys
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
8
10
|
# By default, loops in a dag aren't allowed.
|
|
@@ -67,10 +69,11 @@ class _BlockContext:
|
|
|
67
69
|
|
|
68
70
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
69
71
|
if exc_type is None:
|
|
70
|
-
self.block._block_state = BlockState.WAITING if
|
|
72
|
+
self.block._block_state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
|
|
71
73
|
elif exc_type is KeyboardInterrupt:
|
|
72
74
|
self.block_state._block_state = BlockState.INTERRUPTED
|
|
73
|
-
self.dag.
|
|
75
|
+
if not self.dag._is_pyodide:
|
|
76
|
+
self.dag._stopper.event.set()
|
|
74
77
|
print(f'KEYBOARD INTERRUPT IN BLOCK {self.name}')
|
|
75
78
|
else:
|
|
76
79
|
state = BlockState.ERROR
|
|
@@ -87,7 +90,8 @@ class _BlockContext:
|
|
|
87
90
|
|
|
88
91
|
# msg = f'While in {self.block.name}.execute(): {exc_val}'
|
|
89
92
|
# LOGGER.exception(msg)
|
|
90
|
-
self.dag.
|
|
93
|
+
if not self.dag._is_pyodide:
|
|
94
|
+
self.dag._stopper.event.set()
|
|
91
95
|
|
|
92
96
|
if not issubclass(exc_type, BlockError):
|
|
93
97
|
# Convert non-BlockErrors in the block to a BlockError.
|
|
@@ -113,16 +117,51 @@ class _Stopper:
|
|
|
113
117
|
def __repr__(self):
|
|
114
118
|
return f'stopped={self.is_stopped}'
|
|
115
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
|
+
|
|
116
144
|
class Dag:
|
|
117
145
|
"""A directed acyclic graph of blocks."""
|
|
118
146
|
|
|
119
|
-
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):
|
|
120
148
|
self._block_pairs: list[tuple[Block, Block]] = []
|
|
121
|
-
|
|
149
|
+
|
|
122
150
|
self.site = site
|
|
123
151
|
self.title = title
|
|
124
152
|
self.doc = doc
|
|
125
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
|
+
|
|
162
|
+
if not self._is_pyodide:
|
|
163
|
+
self._stopper = _Stopper()
|
|
164
|
+
|
|
126
165
|
# We watch output params to be notified when they are set.
|
|
127
166
|
# Events are queued here.
|
|
128
167
|
#
|
|
@@ -132,6 +171,14 @@ class Dag:
|
|
|
132
171
|
#
|
|
133
172
|
self._block_context = _BlockContext
|
|
134
173
|
|
|
174
|
+
# Set up the logging hook.
|
|
175
|
+
#
|
|
176
|
+
self.logging = _find_logging()
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def _is_pyodide(self) -> bool:
|
|
180
|
+
return '_pyodide' in sys.modules
|
|
181
|
+
|
|
135
182
|
def _for_each_once(self):
|
|
136
183
|
"""Yield each connected block once."""
|
|
137
184
|
|
|
@@ -144,13 +191,13 @@ class Dag:
|
|
|
144
191
|
|
|
145
192
|
def stop(self):
|
|
146
193
|
"""Stop further execution of Block instances in this dag."""
|
|
147
|
-
|
|
148
|
-
|
|
194
|
+
if not self._is_pyodide:
|
|
195
|
+
self._stopper.event.set()
|
|
149
196
|
|
|
150
197
|
def unstop(self):
|
|
151
198
|
"""Enable further execution of Block instances in this dag."""
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
if not self._is_pyodide:
|
|
200
|
+
self._stopper.event.clear()
|
|
154
201
|
|
|
155
202
|
def connect(self, src: Block, dst: Block, *connections: Connection):
|
|
156
203
|
if any(not isinstance(c, Connection) for c in connections):
|
|
@@ -231,7 +278,7 @@ class Dag:
|
|
|
231
278
|
item.values[inp] = new
|
|
232
279
|
self._block_queue.append(item)
|
|
233
280
|
|
|
234
|
-
def execute_after_input(self, block:
|
|
281
|
+
def execute_after_input(self, block: Block, *, dag_logger=None):
|
|
235
282
|
"""Execute the dag after running ``prepare()`` in an input block.
|
|
236
283
|
|
|
237
284
|
After prepare() executes, and the user has possibly
|
|
@@ -243,23 +290,23 @@ class Dag:
|
|
|
243
290
|
|
|
244
291
|
Parameters
|
|
245
292
|
----------
|
|
246
|
-
block:
|
|
293
|
+
block: Block
|
|
247
294
|
The block to restart the dag at.
|
|
248
295
|
dag_logger:
|
|
249
296
|
A logger adapter that will accept log messages.
|
|
250
297
|
"""
|
|
251
298
|
|
|
252
|
-
if not
|
|
253
|
-
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}')
|
|
254
301
|
|
|
255
|
-
# Prime the block queue, using
|
|
302
|
+
# Prime the block queue, using _RESTART
|
|
256
303
|
# to indicate that this is a restart, and Block.execute()
|
|
257
304
|
# must be called.
|
|
258
305
|
#
|
|
259
|
-
self._block_queue.appendleft(_InputValues(block, {}))
|
|
306
|
+
self._block_queue.appendleft(_InputValues(block, {_RESTART: True}))
|
|
260
307
|
self.execute(dag_logger=dag_logger)
|
|
261
308
|
|
|
262
|
-
def execute(self, *, dag_logger=None):
|
|
309
|
+
def execute(self, *, dag_logger=None) -> Block|None:
|
|
263
310
|
"""Execute the dag.
|
|
264
311
|
|
|
265
312
|
The dag is executed by iterating through the block event queue
|
|
@@ -267,38 +314,54 @@ class Dag:
|
|
|
267
314
|
update the destination block's input parameters and call
|
|
268
315
|
that block's execute() method.
|
|
269
316
|
|
|
270
|
-
If the current destination block is
|
|
317
|
+
If the current destination block's ``block_pause_execution` is True,
|
|
271
318
|
the loop will call ``block.prepare()` instead of ``block.execute()``,
|
|
272
|
-
then stop
|
|
273
|
-
``dag.execute_after_input()
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
the dag
|
|
277
|
-
output param before the dag's execute() is called
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
280
328
|
"""
|
|
281
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
|
+
|
|
282
340
|
if not self._block_queue:
|
|
283
341
|
# Attempting to execute a dag with no updates is probably a mistake.
|
|
284
342
|
#
|
|
285
343
|
raise BlockError('Nothing to execute')
|
|
286
344
|
|
|
345
|
+
self.logging(None, sier2_dag_=self)
|
|
346
|
+
|
|
287
347
|
can_execute = True
|
|
288
348
|
while self._block_queue:
|
|
289
349
|
# print(len(self._block_queue), self._block_queue)
|
|
290
350
|
# The user has set the "stop executing" flag.
|
|
291
351
|
# Continue to set params, but don't execute anything
|
|
292
352
|
#
|
|
293
|
-
if self.
|
|
294
|
-
|
|
353
|
+
if not self._is_pyodide:
|
|
354
|
+
if self._stopper.is_stopped:
|
|
355
|
+
can_execute = False
|
|
295
356
|
|
|
296
357
|
item = self._block_queue.popleft()
|
|
358
|
+
is_restart = item.values.pop(_RESTART, False)
|
|
297
359
|
try:
|
|
298
360
|
item.dst.param.update(item.values)
|
|
299
361
|
except ValueError as e:
|
|
300
362
|
msg = f'While in {item.dst.name} setting a parameter: {e}'
|
|
301
|
-
self.
|
|
363
|
+
if not self._is_pyodide:
|
|
364
|
+
self._stopper.event.set()
|
|
302
365
|
raise BlockError(msg) from e
|
|
303
366
|
|
|
304
367
|
# Execute the block.
|
|
@@ -306,18 +369,27 @@ class Dag:
|
|
|
306
369
|
# unless this is after the user has selected the "Continue"
|
|
307
370
|
# button.
|
|
308
371
|
#
|
|
309
|
-
is_input_block =
|
|
372
|
+
is_input_block = item.dst.block_pause_execution
|
|
310
373
|
if can_execute:
|
|
311
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
|
+
|
|
312
381
|
# If this is an input block, and there are input
|
|
313
382
|
# values, call prepare() if it exists.
|
|
314
383
|
#
|
|
315
|
-
if is_input_block and item.values:
|
|
316
|
-
g.prepare()
|
|
384
|
+
if is_input_block and not is_restart:# and item.values:
|
|
385
|
+
self.logging(g.prepare, **logging_params)()
|
|
317
386
|
else:
|
|
318
|
-
g.execute()
|
|
387
|
+
self.logging(g.execute, **logging_params)()
|
|
319
388
|
|
|
320
|
-
|
|
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:
|
|
321
393
|
# If the current destination block requires user input,
|
|
322
394
|
# stop executing the dag immediately, because we don't
|
|
323
395
|
# want to be setting the input params of further blocks
|
|
@@ -326,7 +398,9 @@ class Dag:
|
|
|
326
398
|
# This possibly leaves items on the queue, which will be
|
|
327
399
|
# executed on the next call to execute().
|
|
328
400
|
#
|
|
329
|
-
|
|
401
|
+
return item.dst
|
|
402
|
+
|
|
403
|
+
return None
|
|
330
404
|
|
|
331
405
|
def disconnect(self, g: Block) -> None:
|
|
332
406
|
"""Disconnect block g from other blocks.
|
|
@@ -372,7 +446,7 @@ class Dag:
|
|
|
372
446
|
|
|
373
447
|
return None
|
|
374
448
|
|
|
375
|
-
def get_sorted(self):
|
|
449
|
+
def get_sorted(self) -> list[Block]:
|
|
376
450
|
"""Return the blocks in this dag in topological order.
|
|
377
451
|
|
|
378
452
|
This is useful for arranging the blocks in a GUI, for example.
|
|
@@ -611,7 +685,7 @@ def _has_cycle(block_pairs: list[tuple[Block, Block]]):
|
|
|
611
685
|
|
|
612
686
|
return len(remaining)>0
|
|
613
687
|
|
|
614
|
-
def _get_sorted(block_pairs: list[tuple[Block, Block]]):
|
|
688
|
+
def _get_sorted(block_pairs: list[tuple[Block, Block]]) -> list[Block]:
|
|
615
689
|
ordered, remaining = topological_sort(block_pairs)
|
|
616
690
|
|
|
617
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,8 +4,9 @@ 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
|
|
@@ -22,7 +23,26 @@ INFO_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" vie
|
|
|
22
23
|
</svg>
|
|
23
24
|
'''
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
if '_pyodide' in sys.modules:
|
|
27
|
+
# Pyodide (to be specific, WASM) doesn't allow threads.
|
|
28
|
+
# Specifying one thread for panel for some reason tries to start one, so we need to rely on the default.
|
|
29
|
+
#
|
|
30
|
+
pn.extension(
|
|
31
|
+
'floatpanel',
|
|
32
|
+
inline=True,
|
|
33
|
+
loading_spinner='bar',
|
|
34
|
+
notifications=True,
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
pn.extension(
|
|
38
|
+
'floatpanel',
|
|
39
|
+
inline=True,
|
|
40
|
+
nthreads=NTHREADS,
|
|
41
|
+
loading_spinner='bar',
|
|
42
|
+
notifications=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
26
46
|
|
|
27
47
|
def _hms(sec):
|
|
28
48
|
h, sec = divmod(int(sec), 3600)
|
|
@@ -54,26 +74,27 @@ class _PanelContext:
|
|
|
54
74
|
block_logger = getBlockPanelLogger(self.block.name)
|
|
55
75
|
self.block.logger = block_logger
|
|
56
76
|
|
|
57
|
-
if self.block._progress:
|
|
58
|
-
|
|
77
|
+
# if self.block._progress:
|
|
78
|
+
# self.block._progress.active = True
|
|
59
79
|
|
|
60
80
|
return self.block
|
|
61
81
|
|
|
62
82
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
63
83
|
delta = (datetime.now() - self.t0).total_seconds()
|
|
64
84
|
|
|
65
|
-
if self.block._progress:
|
|
66
|
-
|
|
85
|
+
# if self.block._progress:
|
|
86
|
+
# self.block._progress.active = False
|
|
67
87
|
|
|
68
88
|
if exc_type is None:
|
|
69
|
-
state = BlockState.WAITING if
|
|
89
|
+
state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
|
|
70
90
|
self.block._block_state = state
|
|
71
91
|
if self.dag_logger:
|
|
72
92
|
self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
|
|
73
93
|
elif isinstance(exc_type, KeyboardInterrupt):
|
|
74
94
|
state = BlockState.INTERRUPTED
|
|
75
95
|
self.block_state._block_state = state
|
|
76
|
-
self.dag.
|
|
96
|
+
if not self.dag._is_pyodide:
|
|
97
|
+
self.dag._stopper.event.set()
|
|
77
98
|
if self.dag_logger:
|
|
78
99
|
self.dag_logger.exception(f'KEYBOARD INTERRUPT after {_hms(delta)}', block_name=self.block.name, block_state=state)
|
|
79
100
|
else:
|
|
@@ -88,13 +109,13 @@ class _PanelContext:
|
|
|
88
109
|
)
|
|
89
110
|
|
|
90
111
|
# msg = f'While in {self.block.name}.execute(): {exc_val}'
|
|
91
|
-
self.dag.
|
|
112
|
+
if not self.dag._is_pyodide:
|
|
113
|
+
self.dag._stopper.event.set()
|
|
92
114
|
|
|
93
115
|
if not issubclass(exc_type, BlockError):
|
|
94
116
|
# Convert the error in the block to a BlockError.
|
|
95
117
|
#
|
|
96
118
|
raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
|
|
97
|
-
|
|
98
119
|
return False
|
|
99
120
|
|
|
100
121
|
def _quit(session_context):
|
|
@@ -116,7 +137,7 @@ def interrupt_thread(tid, exctype):
|
|
|
116
137
|
#
|
|
117
138
|
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
|
|
118
139
|
raise SystemError('PyThreadState_SetAsyncExc failed')
|
|
119
|
-
|
|
140
|
+
|
|
120
141
|
def _prepare_to_show(dag: Dag):
|
|
121
142
|
# Replace the default text-based context with the panel-based context.
|
|
122
143
|
#
|
|
@@ -227,6 +248,13 @@ def _show_dag(dag: Dag):
|
|
|
227
248
|
|
|
228
249
|
pn.state.on_session_destroyed(_quit)
|
|
229
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
|
+
|
|
230
258
|
template.show(threaded=False)
|
|
231
259
|
|
|
232
260
|
def _serveable_dag(dag: Dag):
|
|
@@ -236,8 +264,25 @@ def _serveable_dag(dag: Dag):
|
|
|
236
264
|
|
|
237
265
|
pn.state.on_session_destroyed(_quit)
|
|
238
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
|
+
|
|
239
274
|
template.servable()
|
|
240
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
|
+
|
|
241
286
|
class BlockCard(pn.Card):
|
|
242
287
|
"""A custom card to wrap around a block.
|
|
243
288
|
|
|
@@ -274,19 +319,22 @@ class BlockCard(pn.Card):
|
|
|
274
319
|
# inspect the class and display the param attributes.
|
|
275
320
|
# This is obviously not what we want.
|
|
276
321
|
#
|
|
277
|
-
#
|
|
278
|
-
# The Panel context manager will activate and deactivate it.
|
|
322
|
+
# We just want to display the in_ params.
|
|
279
323
|
#
|
|
280
324
|
has_panel = '__panel__' in w.__class__.__dict__
|
|
281
325
|
if not has_panel:
|
|
282
|
-
w._progress = pn.indicators.Progress(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
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)
|
|
288
336
|
|
|
289
|
-
if
|
|
337
|
+
if w.block_pause_execution:
|
|
290
338
|
# This is an input block, so add a 'Continue' button.
|
|
291
339
|
#
|
|
292
340
|
def on_continue(_event):
|
|
@@ -296,8 +344,9 @@ class BlockCard(pn.Card):
|
|
|
296
344
|
# current values on the queue.
|
|
297
345
|
# If their values are already there, it doesn't matter.
|
|
298
346
|
#
|
|
299
|
-
w.param.trigger(*w._block_out_params)
|
|
300
347
|
parent_template.main[0].loading = True
|
|
348
|
+
w.param.trigger(*w._block_out_params)
|
|
349
|
+
|
|
301
350
|
try:
|
|
302
351
|
if dag_logger:
|
|
303
352
|
dag_logger.info('', block_name=None, block_state=None)
|
|
@@ -320,7 +369,6 @@ class BlockCard(pn.Card):
|
|
|
320
369
|
pn.state.notifications.error(notif, duration=0)
|
|
321
370
|
finally:
|
|
322
371
|
parent_template.main[0].loading = False
|
|
323
|
-
|
|
324
372
|
c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
|
|
325
373
|
c_button.on_click(on_continue)
|
|
326
374
|
|
|
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
|