sier2 0.17__py3-none-any.whl → 0.22__py3-none-any.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.

Potentially problematic release.


This version of sier2 might be problematic. Click here for more details.

sier2/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from ._block import Block, BlockError
1
+ from ._block import Block, InputBlock, BlockError, BlockValidateError
2
2
  from ._dag import Connection, Dag, BlockState
3
3
  from ._library import Library, Info
4
4
  from ._version import __version__
sier2/_block.py CHANGED
@@ -1,13 +1,16 @@
1
1
  from enum import StrEnum
2
2
  import inspect
3
3
  import param
4
- from typing import Any, Callable
5
- from collections import defaultdict
4
+ from typing import Any
6
5
 
7
6
  from . import _logger
8
7
 
9
8
  class BlockError(Exception):
10
- """Raised if a Block configuration is invalid."""
9
+ """Raised if a Block configuration is invalid.
10
+
11
+ If this exception is raised, the executing dag sets its stop
12
+ flag (which must be manually reset), and displays a stacktrace.
13
+ """
11
14
 
12
15
  pass
13
16
 
@@ -50,14 +53,14 @@ class Block(param.Parameterized):
50
53
 
51
54
  SIER2_KEY = '_sier2__key'
52
55
 
53
- def __init__(self, *args, user_input=False, **kwargs):
56
+ def __init__(self, *args, continue_label='Continue', **kwargs):
54
57
  super().__init__(*args, **kwargs)
55
58
 
56
59
  if not self.__doc__:
57
60
  raise BlockError(f'Class {self.__class__} must have a docstring')
58
61
 
59
- self.user_input = user_input
60
- self._block_state = BlockState.INPUT if user_input else BlockState.READY
62
+ self.continue_label = continue_label
63
+ # self._block_state = BlockState.READY
61
64
  self.logger = _logger.get_logger(self.name)
62
65
 
63
66
  # Maintain a map of "block+output parameter being watched" -> "input parameter".
@@ -66,7 +69,7 @@ class Block(param.Parameterized):
66
69
  self._block_name_map: dict[tuple[str, str], str] = {}
67
70
 
68
71
  # Record this block's output parameters.
69
- # If this is a user_input block, we need to trigger
72
+ # If this is an input block, we need to trigger
70
73
  # the output values before executing the next block,
71
74
  # in case the user didn't change anything.
72
75
  #
@@ -146,3 +149,46 @@ class Block(param.Parameterized):
146
149
  result = {name: getattr(self, name) for name in out_names}
147
150
 
148
151
  return result
152
+
153
+ class InputBlock(Block):
154
+ """A ``Block`` that accepts user input.
155
+
156
+ An ``InputBlock`` executes in two steps().
157
+
158
+ When the block is executed by a dag, the dag first sets the input
159
+ params, then calls ``prepare()``. Execution of the dag then stops.
160
+
161
+ The dag is then restarted using ``dag.execute_after_input(input_block)``.
162
+ (An input block must be specified because it is not required that the
163
+ same input block be used immediately.) This causes the block's
164
+ ``execute()`` method to be called without resetting the input params.
165
+
166
+ Dag execution then continues as normal.
167
+ """
168
+
169
+ def __init__(self, *args, continue_label='Continue', **kwargs):
170
+ super().__init__(*args, continue_label=continue_label, **kwargs)
171
+ self._block_state = BlockState.INPUT
172
+
173
+ def prepare(self):
174
+ """Called by a dag before calling ``execute()```.
175
+
176
+ This gives the block author an opportunity to validate the
177
+ input params and set up a user inteface.
178
+
179
+ After the dag restarts on this block, ``execute()`` will be called.
180
+ """
181
+
182
+ pass
183
+
184
+ class BlockValidateError(BlockError):
185
+ """Raised if ``InputBlock.prepare()`` or ``Block.execute()`` determines that input data is invalid.
186
+
187
+ If this exception is raised, it will be caught by the executing dag.
188
+ The dag will not set its stop flag, no stacktrace will be displayed,
189
+ and the error message will be displayed.
190
+ """
191
+
192
+ def __init__(self, block_name: str, error: str):
193
+ super().__init__(error)
194
+ self.block_name = block_name
sier2/_dag.py CHANGED
@@ -1,4 +1,4 @@
1
- from ._block import Block, BlockError, BlockState
1
+ from ._block import Block, InputBlock, 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
@@ -28,7 +28,8 @@ class _InputValues:
28
28
  """Record a param value change.
29
29
 
30
30
  When a block updates an output param, the update is queued until
31
- the block finishes executing. This class is what is queued.
31
+ the block finishes executing. Instances of this class are
32
+ what is queued.
32
33
  """
33
34
 
34
35
  # The block to be updated.
@@ -36,6 +37,9 @@ class _InputValues:
36
37
  dst: Block
37
38
 
38
39
  # The values to be set before the block executes.
40
+ # For a normal block, values will be non-empty when execute() is called.
41
+ # For an input block, if values is non-empty, prepare()
42
+ # will be called, else execute() will be called
39
43
  #
40
44
  values: dict[str, Any] = field(default_factory=dict)
41
45
 
@@ -63,21 +67,35 @@ class _BlockContext:
63
67
 
64
68
  def __exit__(self, exc_type, exc_val, exc_tb):
65
69
  if exc_type is None:
66
- self.block._block_state = BlockState.WAITING if self.block.user_input else BlockState.SUCCESSFUL
67
- elif isinstance(exc_type, KeyboardInterrupt):
70
+ self.block._block_state = BlockState.WAITING if isinstance(self.block, InputBlock) else BlockState.SUCCESSFUL
71
+ elif exc_type is KeyboardInterrupt:
68
72
  self.block_state._block_state = BlockState.INTERRUPTED
69
73
  self.dag._stopper.event.set()
70
74
  print(f'KEYBOARD INTERRUPT IN BLOCK {self.name}')
71
75
  else:
72
- self.block._block_state = BlockState.ERROR
73
- msg = f'While in {self.block.name}.execute(): {exc_val}'
74
- # LOGGER.exception(msg)
75
- self.dag._stopper.event.set()
76
-
77
- # Convert the error in the block to a BlockError.
78
- #
79
- raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
80
-
76
+ state = BlockState.ERROR
77
+ self.block._block_state = state
78
+ if exc_type is not BlockValidateError:
79
+ # Validation errors don't set the stopper;
80
+ # they just stop execution.
81
+ #
82
+ if self.dag_logger:
83
+ self.dag_logger.exception(
84
+ block_name=self.block.name,
85
+ block_state=state
86
+ )
87
+
88
+ # msg = f'While in {self.block.name}.execute(): {exc_val}'
89
+ # LOGGER.exception(msg)
90
+ self.dag._stopper.event.set()
91
+
92
+ if not issubclass(exc_type, BlockError):
93
+ # Convert non-BlockErrors in the block to a BlockError.
94
+ #
95
+ raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
96
+
97
+ # Don't suppress the original exception.
98
+ #
81
99
  return False
82
100
 
83
101
  class _Stopper:
@@ -213,21 +231,52 @@ class Dag:
213
231
  item.values[inp] = new
214
232
  self._block_queue.append(item)
215
233
 
234
+ def execute_after_input(self, block: InputBlock, *, dag_logger=None):
235
+ """Execute the dag after running ``prepare()`` in an input block.
236
+
237
+ After prepare() executes, and the user has possibly
238
+ provided input, the dag must continue with execute() in the
239
+ same block.
240
+
241
+ This method will prime the block queue with the specified block's
242
+ output, and call execute().
243
+
244
+ Parameters
245
+ ----------
246
+ block: InputBlock
247
+ The block to restart the dag at.
248
+ dag_logger:
249
+ A logger adapter that will accept log messages.
250
+ """
251
+
252
+ if not isinstance(block, InputBlock):
253
+ raise BlockError(f'A dag can only restart an InputBlock, not {block.name}')
254
+
255
+ # Prime the block queue, using an empty input param values dict
256
+ # to indicate that this is a restart, and Block.execute()
257
+ # must be called.
258
+ #
259
+ self._block_queue.appendleft(_InputValues(block, {}))
260
+ self.execute(dag_logger=dag_logger)
261
+
216
262
  def execute(self, *, dag_logger=None):
217
263
  """Execute the dag.
218
264
 
219
- The dag is executed by iterating through the block events queue
265
+ The dag is executed by iterating through the block event queue
220
266
  and popping events from the head of the queue. For each event,
221
267
  update the destination block's input parameters and call
222
268
  that block's execute() method.
223
269
 
224
- If the current destination block has user_flag True,
225
- the loop will continue to set param values until the queue is empty,
226
- but no execute() method will be called.
270
+ If the current destination block is an ``InputBlock``,
271
+ the loop will call ``block.prepare()` instead of ``block.execute()``,
272
+ then stop. The dag can then be restarted with
273
+ ``dag.execute_after_input()``.
227
274
 
228
- To start (or restart) the dag, there must be something in the event queue.
229
- The first (or current) user_input block must have updated at least one
230
- output param before the dag's execute() is called.
275
+ To start the dag, there must be something in the event queue -
276
+ the dag must be "primed". A block must have updated at least one
277
+ output param before the dag's execute() is called. Calling
278
+ ``dag.execute()`` will then execute the dag starting with
279
+ the blocks connected to the first block.
231
280
  """
232
281
 
233
282
  if not self._block_queue:
@@ -252,15 +301,32 @@ class Dag:
252
301
  self._stopper.event.set()
253
302
  raise BlockError(msg) from e
254
303
 
304
+ # Execute the block.
305
+ # Don't execute input blocks when we get to them,
306
+ # unless this is after the user has selected the "Continue"
307
+ # button.
308
+ #
309
+ is_input_block = isinstance(item.dst, InputBlock)
255
310
  if can_execute:
256
311
  with self._block_context(block=item.dst, dag=self, dag_logger=dag_logger) as g:
257
- g.execute()
258
-
259
- if item.dst.user_input:
312
+ # If this is an input block, and there are input
313
+ # values, call prepare() if it exists.
314
+ #
315
+ if is_input_block and item.values:
316
+ g.prepare()
317
+ else:
318
+ g.execute()
319
+
320
+ if is_input_block and item.values:
260
321
  # If the current destination block requires user input,
261
- # continue to set params, but don't execute anything.
322
+ # stop executing the dag immediately, because we don't
323
+ # want to be setting the input params of further blocks
324
+ # and causing them to do things.
262
325
  #
263
- can_execute = False
326
+ # This possibly leaves items on the queue, which will be
327
+ # executed on the next call to execute().
328
+ #
329
+ break
264
330
 
265
331
  def disconnect(self, g: Block) -> None:
266
332
  """Disconnect block g from other blocks.
@@ -373,10 +439,6 @@ class Dag:
373
439
  if hasattr(g, var):
374
440
  args[var] = getattr(g, var)
375
441
 
376
- # TODO is there a better way of checking for user_input?
377
- if hasattr(g, 'user_input'):
378
- args['user_input'] = getattr(g, 'user_input')
379
-
380
442
  block = {
381
443
  'block': g.block_key(),
382
444
  'instance': i,
sier2/panel/_panel.py CHANGED
@@ -1,10 +1,12 @@
1
1
  import ctypes
2
2
  from datetime import datetime
3
+ import html
3
4
  import panel as pn
4
5
  import sys
5
6
  import threading
6
7
 
7
- from sier2 import Block, BlockState, Dag, BlockError
8
+ from sier2 import Block, InputBlock, BlockValidateError, BlockState, Dag, BlockError
9
+ from .._dag import _InputValues
8
10
  from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
9
11
  from ._panel_util import _get_state_color, dag_doc
10
12
 
@@ -20,7 +22,7 @@ INFO_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" vie
20
22
  </svg>
21
23
  '''
22
24
 
23
- pn.extension('floatpanel', inline=True, nthreads=NTHREADS, loading_spinner='bar')
25
+ pn.extension('floatpanel', inline=True, nthreads=NTHREADS, loading_spinner='bar', notifications=True)
24
26
 
25
27
  def _hms(sec):
26
28
  h, sec = divmod(int(sec), 3600)
@@ -64,7 +66,7 @@ class _PanelContext:
64
66
  self.block._progress.active = False
65
67
 
66
68
  if exc_type is None:
67
- state = BlockState.WAITING if self.block.user_input else BlockState.SUCCESSFUL
69
+ state = BlockState.WAITING if isinstance(self.block, InputBlock) else BlockState.SUCCESSFUL
68
70
  self.block._block_state = state
69
71
  if self.dag_logger:
70
72
  self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
@@ -77,14 +79,21 @@ class _PanelContext:
77
79
  else:
78
80
  state = BlockState.ERROR
79
81
  self.block._block_state = state
80
- if self.dag_logger:
81
- self.dag_logger.exception(f'after {_hms(delta)}', block_name=self.block.name, block_state=state)
82
- msg = f'While in {self.block.name}.execute(): {exc_val}'
83
- self.dag._stopper.event.set()
84
-
85
- # Convert the error in the block to a BlockError.
86
- #
87
- raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
82
+ if exc_type is not BlockValidateError:
83
+ if self.dag_logger:
84
+ self.dag_logger.exception(
85
+ f'after {_hms(delta)}',
86
+ block_name=self.block.name,
87
+ block_state=state
88
+ )
89
+
90
+ # msg = f'While in {self.block.name}.execute(): {exc_val}'
91
+ self.dag._stopper.event.set()
92
+
93
+ if not issubclass(exc_type, BlockError):
94
+ # Convert the error in the block to a BlockError.
95
+ #
96
+ raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
88
97
 
89
98
  return False
90
99
 
@@ -122,7 +131,9 @@ def _show_dag(dag: Dag):
122
131
  align='center'
123
132
  )
124
133
 
125
- fp_holder = pn.Column(visible=False)
134
+ # A place to stash the info FloatPanel.
135
+ #
136
+ info_fp_holder = pn.Column(visible=False)
126
137
 
127
138
  sidebar_title = pn.Row(info_button, '## Blocks')
128
139
  template = pn.template.BootstrapTemplate(
@@ -143,7 +154,8 @@ def _show_dag(dag: Dag):
143
154
  'contentOverflow': 'scroll'
144
155
  }
145
156
  fp = pn.layout.FloatPanel(text, name=dag.title, width=550, height=450, contained=False, position='center', theme='dark filleddark', config=config)
146
- fp_holder[:] = [fp]
157
+ info_fp_holder[:] = [fp]
158
+
147
159
  info_button.on_click(display_info)
148
160
 
149
161
  switch = pn.widgets.Switch(name='Stop')
@@ -204,7 +216,7 @@ def _show_dag(dag: Dag):
204
216
  switch,
205
217
  pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
206
218
  log_feed,
207
- fp_holder
219
+ info_fp_holder
208
220
  )
209
221
  )
210
222
 
@@ -260,8 +272,8 @@ class BlockCard(pn.Card):
260
272
  value=-1
261
273
  )
262
274
 
263
- if w.user_input:
264
- # This is a user_input block, so add a 'Continue' button.
275
+ if isinstance(w, InputBlock):
276
+ # This is an input block, so add a 'Continue' button.
265
277
  #
266
278
  def on_continue(_event):
267
279
  # The user may not have changed anything from the default values,
@@ -276,12 +288,27 @@ class BlockCard(pn.Card):
276
288
  if dag_logger:
277
289
  dag_logger.info('', block_name=None, block_state=None)
278
290
  dag_logger.info('Execute dag', block_name='', block_state=BlockState.DAG)
279
- dag.execute(dag_logger=dag_logger)
291
+
292
+ # We want this block's execute() method to run first
293
+ # after the user clicks the "Continue" button.
294
+ # We make this happen by pushing this block on the head
295
+ # of the queue, but without any values - we don't want
296
+ # to trigger any param changes.
297
+ #
298
+ try:
299
+ dag.execute_after_input(w, dag_logger=dag_logger)
300
+ except BlockValidateError as e:
301
+ # Display the error as a notification.
302
+ #
303
+ block_name = html.escape(e.block_name)
304
+ error = html.escape(str(e))
305
+ notif = f'<b>{block_name}</b>:<br>{error}'
306
+ pn.state.notifications.error(notif, duration=0)
280
307
  finally:
281
308
  parent_template.main[0].loading = False
282
309
 
283
- c_button = pn.widgets.Button(name='Continue', button_type='primary')
284
- pn.bind(on_continue, c_button, watch=True)
310
+ c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
311
+ c_button.on_click(on_continue)
285
312
 
286
313
  w_ = pn.Column(
287
314
  w,
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sier2
3
- Version: 0.17
4
- Summary: Block code dags
3
+ Version: 0.22
4
+ Summary: Blocks of code that are executed in dags
5
5
  Author: algol60
6
6
  Author-email: algol60@users.noreply.github.com
7
7
  Requires-Python: >=3.11,<4.0
8
8
  Classifier: Intended Audience :: Developers
9
9
  Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
10
11
  Classifier: Operating System :: OS Independent
11
12
  Classifier: Programming Language :: Python :: 3
12
13
  Classifier: Programming Language :: Python :: 3.11
@@ -14,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
15
16
  Classifier: Topic :: Scientific/Engineering
16
17
  Classifier: Topic :: Software Development :: Libraries
17
- Requires-Dist: holoviews (>=1.18.3,<2.0.0)
18
+ Requires-Dist: holoviews (>=1.19.0)
18
19
  Requires-Dist: panel (>=1.4.4)
19
20
  Requires-Dist: param (>=2.1.0)
20
21
  Description-Content-Type: text/x-rst
@@ -1,16 +1,16 @@
1
- sier2/__init__.py,sha256=evhmBhh3aIfP-yu72pBZbv-3ZwillUHLxqip74zdvBg,154
1
+ sier2/__init__.py,sha256=t1dLAlWzyhcRxkH-lcmN4NmPFs1Z57iQ93HBO8Fcpmo,186
2
2
  sier2/__main__.py,sha256=HZfzJLaD2_JOyKFkFYTD2vs-UARxNMjP4D7ZdJg405A,3140
3
- sier2/_block.py,sha256=JEfjrMydPKBHZBgzjfkLYkW30r5ZeTH3leFg6RoAx0g,4955
4
- sier2/_dag.py,sha256=1zOdaVInZVFkttETWBwbq_L7ii7YeO5qH_mNF7BYQx4,18668
3
+ sier2/_block.py,sha256=wEXIk5UtV7utYSUNzQuXshgkKg6dVb7SGm4E6tfoTmM,6590
4
+ sier2/_dag.py,sha256=KlLvGImFZoJfzgjRHJ7l-mpkUtPUdiyFgAbdz8WJF-Y,21278
5
5
  sier2/_library.py,sha256=aG1f6xHE2qtzvlFCgIp0cMXd6OKlkW_OvbQQb-b2l_8,7536
6
6
  sier2/_logger.py,sha256=lwRmYjXMkP7TyURo5gvOf266vwGDFkLGau-EXpo5eEw,2379
7
7
  sier2/_util.py,sha256=NmXI7QMSdkoSMe6EYJ-q8zI9iGJeMUto3g4314UVoM8,1932
8
8
  sier2/_version.py,sha256=K5EdVMOTOHqhr-mIMjXhh84WHTSES2K-MJ_b--KryBM,71
9
9
  sier2/panel/__init__.py,sha256=wDEf_v859flQX4udAVYZW1m79sfB1NIrI3pyNIpNiEM,29
10
10
  sier2/panel/_feedlogger.py,sha256=tsrA8R2FZUecVY2egifVu2qosRfjccgvGRE0lLZSXZY,5270
11
- sier2/panel/_panel.py,sha256=g68zXfyqzcTiBlaC9n_UXTlWtJDdDNCIwLqmJEjgyTQ,10890
11
+ sier2/panel/_panel.py,sha256=XdkpUlXd2dkVhZrJiIBlDRbCvoabZkmuK3sHd8w4F4M,12118
12
12
  sier2/panel/_panel_util.py,sha256=omcLO0OIHhH00l9YXv09Qv8lnaY6VKsQ1F0qbsrs3vk,2450
13
- sier2-0.17.dist-info/LICENSE,sha256=2AKq0yxLLDdGsj6xQuNjDPG5d2IbFWFGiB_cnCBtMp4,1064
14
- sier2-0.17.dist-info/METADATA,sha256=TBbD_gpxkuM0dHoOZEeYScUy_xtD-rn7SqWs-33hk9Y,2423
15
- sier2-0.17.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
16
- sier2-0.17.dist-info/RECORD,,
13
+ sier2-0.22.dist-info/LICENSE,sha256=2AKq0yxLLDdGsj6xQuNjDPG5d2IbFWFGiB_cnCBtMp4,1064
14
+ sier2-0.22.dist-info/METADATA,sha256=-qzCWs4dbWiT2haxDg2V9H0h0340IbpYsx2Y_dUd2P8,2492
15
+ sier2-0.22.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
16
+ sier2-0.22.dist-info/RECORD,,
File without changes
File without changes