sier2 0.17__py3-none-any.whl → 0.23__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
 
@@ -107,10 +116,8 @@ def interrupt_thread(tid, exctype):
107
116
  #
108
117
  ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
109
118
  raise SystemError('PyThreadState_SetAsyncExc failed')
110
-
111
- def _show_dag(dag: Dag):
112
- """Display the dag in a panel template."""
113
-
119
+
120
+ def _prepare_to_show(dag: Dag):
114
121
  # Replace the default text-based context with the panel-based context.
115
122
  #
116
123
  dag._block_context = _PanelContext
@@ -122,7 +129,9 @@ def _show_dag(dag: Dag):
122
129
  align='center'
123
130
  )
124
131
 
125
- fp_holder = pn.Column(visible=False)
132
+ # A place to stash the info FloatPanel.
133
+ #
134
+ info_fp_holder = pn.Column(visible=False)
126
135
 
127
136
  sidebar_title = pn.Row(info_button, '## Blocks')
128
137
  template = pn.template.BootstrapTemplate(
@@ -143,7 +152,8 @@ def _show_dag(dag: Dag):
143
152
  'contentOverflow': 'scroll'
144
153
  }
145
154
  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]
155
+ info_fp_holder[:] = [fp]
156
+
147
157
  info_button.on_click(display_info)
148
158
 
149
159
  switch = pn.widgets.Switch(name='Stop')
@@ -204,14 +214,30 @@ def _show_dag(dag: Dag):
204
214
  switch,
205
215
  pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
206
216
  log_feed,
207
- fp_holder
217
+ info_fp_holder
208
218
  )
209
219
  )
210
220
 
221
+ return template
222
+
223
+ def _show_dag(dag: Dag):
224
+ """Display the dag in a panel template."""
225
+
226
+ template = _prepare_to_show(dag)
227
+
211
228
  pn.state.on_session_destroyed(_quit)
212
229
 
213
230
  template.show(threaded=False)
214
231
 
232
+ def _serveable_dag(dag: Dag):
233
+ """Serve the dag in a panel template."""
234
+
235
+ template = _prepare_to_show(dag)
236
+
237
+ pn.state.on_session_destroyed(_quit)
238
+
239
+ template.servable()
240
+
215
241
  class BlockCard(pn.Card):
216
242
  """A custom card to wrap around a block.
217
243
 
@@ -260,8 +286,8 @@ class BlockCard(pn.Card):
260
286
  value=-1
261
287
  )
262
288
 
263
- if w.user_input:
264
- # This is a user_input block, so add a 'Continue' button.
289
+ if isinstance(w, InputBlock):
290
+ # This is an input block, so add a 'Continue' button.
265
291
  #
266
292
  def on_continue(_event):
267
293
  # The user may not have changed anything from the default values,
@@ -276,12 +302,27 @@ class BlockCard(pn.Card):
276
302
  if dag_logger:
277
303
  dag_logger.info('', block_name=None, block_state=None)
278
304
  dag_logger.info('Execute dag', block_name='', block_state=BlockState.DAG)
279
- dag.execute(dag_logger=dag_logger)
305
+
306
+ # We want this block's execute() method to run first
307
+ # after the user clicks the "Continue" button.
308
+ # We make this happen by pushing this block on the head
309
+ # of the queue, but without any values - we don't want
310
+ # to trigger any param changes.
311
+ #
312
+ try:
313
+ dag.execute_after_input(w, dag_logger=dag_logger)
314
+ except BlockValidateError as e:
315
+ # Display the error as a notification.
316
+ #
317
+ block_name = html.escape(e.block_name)
318
+ error = html.escape(str(e))
319
+ notif = f'<b>{block_name}</b>:<br>{error}'
320
+ pn.state.notifications.error(notif, duration=0)
280
321
  finally:
281
322
  parent_template.main[0].loading = False
282
323
 
283
- c_button = pn.widgets.Button(name='Continue', button_type='primary')
284
- pn.bind(on_continue, c_button, watch=True)
324
+ c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
325
+ c_button.on_click(on_continue)
285
326
 
286
327
  w_ = pn.Column(
287
328
  w,
@@ -318,3 +359,6 @@ class PanelDag(Dag):
318
359
 
319
360
  def show(self):
320
361
  _show_dag(self)
362
+
363
+ def servable(self):
364
+ _serveable_dag(self)
@@ -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.23
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=Q3a5Y1TTkhKzU5n0ZpmKH5MkSDOJAlidVz2Sq3F_cvc,12451
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.23.dist-info/LICENSE,sha256=2AKq0yxLLDdGsj6xQuNjDPG5d2IbFWFGiB_cnCBtMp4,1064
14
+ sier2-0.23.dist-info/METADATA,sha256=ak30FoB2FhtDAuby3K97gbjc7NZkUgNXOPwCAlzYGNI,2492
15
+ sier2-0.23.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
16
+ sier2-0.23.dist-info/RECORD,,
File without changes
File without changes