sier2 1.0.1__py2.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.
- sier2/__init__.py +6 -0
- sier2/__main__.py +111 -0
- sier2/_block.py +273 -0
- sier2/_config.py +251 -0
- sier2/_dag.py +648 -0
- sier2/_library.py +256 -0
- sier2/_logger.py +65 -0
- sier2/_util.py +127 -0
- sier2/_version.py +3 -0
- sier2/panel/__init__.py +1 -0
- sier2/panel/_chart.py +313 -0
- sier2/panel/_feedlogger.py +153 -0
- sier2/panel/_panel.py +505 -0
- sier2/panel/_panel_util.py +83 -0
- sier2-1.0.1.dist-info/METADATA +69 -0
- sier2-1.0.1.dist-info/RECORD +18 -0
- sier2-1.0.1.dist-info/WHEEL +4 -0
- sier2-1.0.1.dist-info/licenses/LICENSE +21 -0
sier2/panel/_panel.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import html
|
|
4
|
+
import panel as pn
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
import param.parameterized as paramp
|
|
11
|
+
from param.parameters import DataFrame
|
|
12
|
+
|
|
13
|
+
from sier2 import Block, BlockValidateError, BlockState, Dag, BlockError
|
|
14
|
+
from .._dag import _InputValues
|
|
15
|
+
from .._util import trim
|
|
16
|
+
from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
|
|
17
|
+
from ._panel_util import _get_state_color, dag_doc
|
|
18
|
+
from ._chart import html_graph
|
|
19
|
+
|
|
20
|
+
NTHREADS = 2
|
|
21
|
+
|
|
22
|
+
# From https://tabler.io/icons/icon/info-circle
|
|
23
|
+
#
|
|
24
|
+
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
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
26
|
+
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
|
27
|
+
<path d="M12 9h.01" />
|
|
28
|
+
<path d="M11 12h1v4h1" />
|
|
29
|
+
</svg>
|
|
30
|
+
'''
|
|
31
|
+
|
|
32
|
+
if '_pyodide' in sys.modules:
|
|
33
|
+
# Pyodide (to be specific, WASM) doesn't allow threads.
|
|
34
|
+
# Specifying one thread for panel for some reason tries to start one, so we need to rely on the default.
|
|
35
|
+
#
|
|
36
|
+
pn.extension(
|
|
37
|
+
'floatpanel',
|
|
38
|
+
inline=True,
|
|
39
|
+
loading_spinner='bar',
|
|
40
|
+
notifications=True,
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
pn.extension(
|
|
44
|
+
'floatpanel',
|
|
45
|
+
inline=True,
|
|
46
|
+
nthreads=NTHREADS,
|
|
47
|
+
loading_spinner='bar',
|
|
48
|
+
notifications=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _hms(sec):
|
|
54
|
+
h, sec = divmod(int(sec), 3600)
|
|
55
|
+
m, sec = divmod(sec, 60)
|
|
56
|
+
|
|
57
|
+
return f'{h:02}:{m:02}:{sec:02}'
|
|
58
|
+
|
|
59
|
+
class _PanelContext:
|
|
60
|
+
"""A context manager to wrap the execution of a block within a dag.
|
|
61
|
+
|
|
62
|
+
This default context manager handles the block state, the stopper,
|
|
63
|
+
and converts block execution errors to BlockError exceptions.
|
|
64
|
+
|
|
65
|
+
It also uses the panel UI to provide extra information to the user.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *, block: Block, dag: Dag, dag_logger=None):
|
|
69
|
+
self.block = block
|
|
70
|
+
self.dag = dag
|
|
71
|
+
self.dag_logger = dag_logger
|
|
72
|
+
|
|
73
|
+
def __enter__(self):
|
|
74
|
+
state = BlockState.EXECUTING
|
|
75
|
+
self.block._block_state = state
|
|
76
|
+
self.t0 = datetime.now()
|
|
77
|
+
if self.dag_logger:
|
|
78
|
+
self.dag_logger.info('Execute', block_name=self.block.name, block_state=state)
|
|
79
|
+
|
|
80
|
+
block_logger = getBlockPanelLogger(self.block.name)
|
|
81
|
+
self.block.logger = block_logger
|
|
82
|
+
|
|
83
|
+
# if self.block._progress:
|
|
84
|
+
# self.block._progress.active = True
|
|
85
|
+
|
|
86
|
+
return self.block
|
|
87
|
+
|
|
88
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
89
|
+
delta = (datetime.now() - self.t0).total_seconds()
|
|
90
|
+
|
|
91
|
+
# if self.block._progress:
|
|
92
|
+
# self.block._progress.active = False
|
|
93
|
+
|
|
94
|
+
if exc_type is None:
|
|
95
|
+
state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
|
|
96
|
+
self.block._block_state = state
|
|
97
|
+
if self.dag_logger:
|
|
98
|
+
self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
|
|
99
|
+
elif isinstance(exc_type, KeyboardInterrupt):
|
|
100
|
+
state = BlockState.INTERRUPTED
|
|
101
|
+
self.block_state._block_state = state
|
|
102
|
+
if not self.dag._is_pyodide:
|
|
103
|
+
self.dag._stopper.event.set()
|
|
104
|
+
if self.dag_logger:
|
|
105
|
+
self.dag_logger.exception(f'KEYBOARD INTERRUPT after {_hms(delta)}', block_name=self.block.name, block_state=state)
|
|
106
|
+
else:
|
|
107
|
+
state = BlockState.ERROR
|
|
108
|
+
self.block._block_state = state
|
|
109
|
+
if exc_type is not BlockValidateError:
|
|
110
|
+
if self.dag_logger:
|
|
111
|
+
self.dag_logger.exception(
|
|
112
|
+
f'after {_hms(delta)}',
|
|
113
|
+
block_name=self.block.name,
|
|
114
|
+
block_state=state
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# msg = f'While in {self.block.name}.execute(): {exc_val}'
|
|
118
|
+
if not self.dag._is_pyodide:
|
|
119
|
+
self.dag._stopper.event.set()
|
|
120
|
+
|
|
121
|
+
if not issubclass(exc_type, BlockError):
|
|
122
|
+
# Convert the error in the block to a BlockError.
|
|
123
|
+
#
|
|
124
|
+
raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def _quit(session_context):
|
|
128
|
+
print(session_context)
|
|
129
|
+
sys.exit()
|
|
130
|
+
|
|
131
|
+
def interrupt_thread(tid, exctype):
|
|
132
|
+
"""Raise exception exctype in thread tid."""
|
|
133
|
+
|
|
134
|
+
r = ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
135
|
+
ctypes.c_ulong(tid),
|
|
136
|
+
ctypes.py_object(exctype)
|
|
137
|
+
)
|
|
138
|
+
if r==0:
|
|
139
|
+
raise ValueError('Invalid thread id')
|
|
140
|
+
elif r!=1:
|
|
141
|
+
# "if it returns a number greater than one, you're in trouble,
|
|
142
|
+
# and you should call it again with exc=NULL to revert the effect"
|
|
143
|
+
#
|
|
144
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
|
|
145
|
+
raise SystemError('PyThreadState_SetAsyncExc failed')
|
|
146
|
+
|
|
147
|
+
def _prepare_to_show(dag: Dag):
|
|
148
|
+
# Replace the default text-based context with the panel-based context.
|
|
149
|
+
#
|
|
150
|
+
dag._block_context = _PanelContext
|
|
151
|
+
|
|
152
|
+
info_button = pn.widgets.ButtonIcon(
|
|
153
|
+
icon=INFO_SVG,
|
|
154
|
+
active_icon=INFO_SVG,
|
|
155
|
+
description='Dag Help',
|
|
156
|
+
align='center'
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# A place to stash the info FloatPanel.
|
|
160
|
+
#
|
|
161
|
+
info_fp_holder = pn.Column(visible=False)
|
|
162
|
+
|
|
163
|
+
sidebar_title = pn.Row(info_button, '## Blocks')
|
|
164
|
+
template = pn.template.BootstrapTemplate(
|
|
165
|
+
site=dag.site,
|
|
166
|
+
title=dag.title,
|
|
167
|
+
theme='dark',
|
|
168
|
+
header_background='#1e2329',
|
|
169
|
+
header_color='#7dd3fc',
|
|
170
|
+
sidebar=pn.Column(sidebar_title),
|
|
171
|
+
collapsed_sidebar=True,
|
|
172
|
+
sidebar_width=440
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def display_info(_event):
|
|
176
|
+
"""Display a FloatPanel containing help for the dag and blocks."""
|
|
177
|
+
|
|
178
|
+
text = dag_doc(dag)
|
|
179
|
+
config = {
|
|
180
|
+
'headerControls': {'maximize': 'remove'},
|
|
181
|
+
'contentOverflow': 'scroll'
|
|
182
|
+
}
|
|
183
|
+
fp = pn.layout.FloatPanel(text, name=dag.title, width=550, height=450, contained=False, position='center', theme='dark filleddark', config=config)
|
|
184
|
+
info_fp_holder[:] = [fp]
|
|
185
|
+
|
|
186
|
+
info_button.on_click(display_info)
|
|
187
|
+
|
|
188
|
+
switch = pn.widgets.Switch(name='Stop')
|
|
189
|
+
|
|
190
|
+
def on_switch(event):
|
|
191
|
+
if switch.value:
|
|
192
|
+
dag.stop()
|
|
193
|
+
reset()
|
|
194
|
+
|
|
195
|
+
# Which thread are we running on?
|
|
196
|
+
#
|
|
197
|
+
current_tid = threading.current_thread().ident
|
|
198
|
+
|
|
199
|
+
# What other threads are running?
|
|
200
|
+
# There are multiple threads running, including the main thread
|
|
201
|
+
# and the bokeh server thread. We need to find the panel threads.
|
|
202
|
+
# Unfortunately, there is nothing special about them.
|
|
203
|
+
#
|
|
204
|
+
print('THREADS', current_tid, [t for t in threading.enumerate()])
|
|
205
|
+
all_threads = [t for t in threading.enumerate() if t.name.startswith('ThreadPoolExecutor')]
|
|
206
|
+
assert len(all_threads)<=NTHREADS, f'{all_threads=}'
|
|
207
|
+
other_thread = [t for t in all_threads if t.ident!=current_tid]
|
|
208
|
+
|
|
209
|
+
# It's possible that since the user might not have done anything yet,
|
|
210
|
+
# another thread hasn't spun up.
|
|
211
|
+
#
|
|
212
|
+
if other_thread:
|
|
213
|
+
interrupt_thread(other_thread[0].ident, KeyboardInterrupt)
|
|
214
|
+
else:
|
|
215
|
+
dag.unstop()
|
|
216
|
+
# TODO reset status for each card
|
|
217
|
+
|
|
218
|
+
pn.bind(on_switch, switch, watch=True)
|
|
219
|
+
|
|
220
|
+
def reset():
|
|
221
|
+
"""Experiment."""
|
|
222
|
+
col = template.main.objects[0]
|
|
223
|
+
for card in col:
|
|
224
|
+
status = card.header[0]
|
|
225
|
+
|
|
226
|
+
# We use a Panel Feed widget to display log messages.
|
|
227
|
+
#
|
|
228
|
+
log_feed = pn.Feed(
|
|
229
|
+
view_latest=True,
|
|
230
|
+
scroll_button_threshold=20,
|
|
231
|
+
auto_scroll_limit=1,
|
|
232
|
+
sizing_mode='stretch_width'
|
|
233
|
+
)
|
|
234
|
+
dag_logger = getDagPanelLogger(log_feed)
|
|
235
|
+
|
|
236
|
+
cards = []
|
|
237
|
+
if dag.show_doc:
|
|
238
|
+
# The first line of the dag doc is the card header.
|
|
239
|
+
#
|
|
240
|
+
doc = dag.doc.strip()
|
|
241
|
+
ix = doc.find('\n')
|
|
242
|
+
if ix>=0:
|
|
243
|
+
header = doc[:ix]
|
|
244
|
+
doc = doc[ix:].strip()
|
|
245
|
+
else:
|
|
246
|
+
header = ''
|
|
247
|
+
|
|
248
|
+
name_text = pn.widgets.StaticText(
|
|
249
|
+
value=header,
|
|
250
|
+
css_classes=['card-title'],
|
|
251
|
+
styles={'font-size':'1.17em', 'font-weight':'bold'}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
card = pn.Card(pn.pane.Markdown(doc, sizing_mode='stretch_width'), header=pn.Row(name_text), sizing_mode='stretch_width')
|
|
255
|
+
cards.append(card)
|
|
256
|
+
|
|
257
|
+
cards.extend(BlockCard(parent_template=template, dag=dag, w=gw, dag_logger=dag_logger) for gw in dag.get_sorted() if gw.block_visible)
|
|
258
|
+
|
|
259
|
+
template.main.append(pn.panel(pn.Column(*cards)))
|
|
260
|
+
template.sidebar.append(
|
|
261
|
+
pn.Column(
|
|
262
|
+
switch,
|
|
263
|
+
# pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
|
|
264
|
+
pn.Row(
|
|
265
|
+
pn.panel(html_graph(dag)),
|
|
266
|
+
max_width=400,
|
|
267
|
+
max_height=200,
|
|
268
|
+
),
|
|
269
|
+
log_feed,
|
|
270
|
+
info_fp_holder
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return template
|
|
275
|
+
|
|
276
|
+
# def _show_dag(dag: Dag):
|
|
277
|
+
# """Display the dag in a panel template."""
|
|
278
|
+
|
|
279
|
+
# template = _prepare_to_show(dag)
|
|
280
|
+
|
|
281
|
+
# pn.state.on_session_destroyed(_quit)
|
|
282
|
+
|
|
283
|
+
# # Execute the dag.
|
|
284
|
+
# # Since this is a panel dag, we expect the first block to be an input nlock.
|
|
285
|
+
# # This ensures that the first block's prepare() method is called.
|
|
286
|
+
# # If the first block is not an input block, it must be primed, just like a plain dag.
|
|
287
|
+
# #
|
|
288
|
+
# dag.execute()
|
|
289
|
+
|
|
290
|
+
# template.show(threaded=False)
|
|
291
|
+
|
|
292
|
+
# def _serveable_dag(dag: Dag):
|
|
293
|
+
# """Serve the dag in a panel template."""
|
|
294
|
+
|
|
295
|
+
# template = _prepare_to_show(dag)
|
|
296
|
+
|
|
297
|
+
# pn.state.on_session_destroyed(_quit)
|
|
298
|
+
|
|
299
|
+
# # Execute the dag.
|
|
300
|
+
# # Since this is a panel dag, we expect the first block to be an input nlock.
|
|
301
|
+
# # This ensures that the first block's prepare() method is called.
|
|
302
|
+
# # If the first block is not an input block, it must be primed, just like a plain dag.
|
|
303
|
+
# #
|
|
304
|
+
# dag.execute()
|
|
305
|
+
|
|
306
|
+
# template.servable()
|
|
307
|
+
|
|
308
|
+
def _default_panel(self) -> Callable[[Block], pn.Param]:
|
|
309
|
+
"""Provide a default __panel__() implementation for blocks that don't have one.param.parameters.
|
|
310
|
+
|
|
311
|
+
This default will display the in_ parameters.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
in_names = [name for name in self.param.values() if name.startswith('in_')]
|
|
315
|
+
|
|
316
|
+
# Check if we need tabulator installed.
|
|
317
|
+
# Ostensibly param uses the DataFrame widget if the tabulator extension isn't present,
|
|
318
|
+
# but this doesn't seem to work properly.
|
|
319
|
+
#
|
|
320
|
+
if any([isinstance(self.param[name], DataFrame) for name in in_names]):
|
|
321
|
+
if 'tabulator' not in pn.extension._loaded_extensions:
|
|
322
|
+
tabulator_warning = f'One of your blocks ({self.__class__.__name__}) requires Tabulator, a panel extension for showing data frames. You should explicitly load this with "pn.extension(\'tabulator\')" in your block'
|
|
323
|
+
warnings.warn(tabulator_warning)
|
|
324
|
+
pn.extension('tabulator')
|
|
325
|
+
|
|
326
|
+
param_pane = pn.Param(self, parameters=in_names, show_name=False)
|
|
327
|
+
|
|
328
|
+
return param_pane
|
|
329
|
+
|
|
330
|
+
class BlockCard(pn.Card):
|
|
331
|
+
"""A custom card to wrap around a block.
|
|
332
|
+
|
|
333
|
+
This adds the block title and a status light to the card header.
|
|
334
|
+
The light updates to match the block state.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def _get_state_light(color: str) -> pn.Spacer:
|
|
339
|
+
return pn.Spacer(
|
|
340
|
+
margin=(8, 0, 0, 0),
|
|
341
|
+
styles={'width':'20px', 'height':'20px', 'background':color, 'border-radius': '10px'}
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# def ui(self, message):
|
|
345
|
+
# """TODO connect this to the template"""
|
|
346
|
+
# print(message)
|
|
347
|
+
|
|
348
|
+
def __init__(self, *args, parent_template, dag: Dag, w: Block, dag_logger=None, **kwargs):
|
|
349
|
+
# Make this look like <h3> (the default Card header text).
|
|
350
|
+
#
|
|
351
|
+
name_text = pn.widgets.StaticText(
|
|
352
|
+
value=w.name,
|
|
353
|
+
css_classes=['card-title'],
|
|
354
|
+
styles={'font-size':'1.17em', 'font-weight':'bold'}
|
|
355
|
+
)
|
|
356
|
+
spacer = pn.HSpacer(
|
|
357
|
+
styles=dict(
|
|
358
|
+
min_width='1px', min_height='1px'
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Does this block have documentation to be displayed in the card?
|
|
363
|
+
#
|
|
364
|
+
doc = pn.pane.Markdown(w.block_doc, sizing_mode='scale_width') if w.block_doc else None
|
|
365
|
+
|
|
366
|
+
# If a block has no __panel__() method, Panel will by default
|
|
367
|
+
# inspect the class and display the param attributes.
|
|
368
|
+
# This is obviously not what we want.
|
|
369
|
+
#
|
|
370
|
+
# We just want to display the in_ params.
|
|
371
|
+
#
|
|
372
|
+
has_panel = '__panel__' in w.__class__.__dict__
|
|
373
|
+
if not has_panel:
|
|
374
|
+
# w._progress = pn.indicators.Progress(
|
|
375
|
+
# name='Block progress',
|
|
376
|
+
# bar_color='primary',
|
|
377
|
+
# active=False,
|
|
378
|
+
# value=-1
|
|
379
|
+
# )
|
|
380
|
+
|
|
381
|
+
# Go go gadget descriptor protocol.
|
|
382
|
+
#
|
|
383
|
+
w.__panel__ = _default_panel.__get__(w)
|
|
384
|
+
|
|
385
|
+
if w.block_pause_execution:
|
|
386
|
+
# This is an input block, so add a 'Continue' button.
|
|
387
|
+
#
|
|
388
|
+
def on_continue(_event):
|
|
389
|
+
# The user may not have changed anything from the default values,
|
|
390
|
+
# so there won't be anything on the block queue.
|
|
391
|
+
# Therefore, we trigger the output params to put their
|
|
392
|
+
# current values on the queue.
|
|
393
|
+
# If their values are already there, it doesn't matter.
|
|
394
|
+
#
|
|
395
|
+
parent_template.main[0].loading = True
|
|
396
|
+
w.param.trigger(*w._block_out_params)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
if dag_logger:
|
|
400
|
+
dag_logger.info('', block_name=None, block_state=None)
|
|
401
|
+
dag_logger.info('Execute dag', block_name='', block_state=BlockState.DAG)
|
|
402
|
+
|
|
403
|
+
# We want this block's execute() method to run first
|
|
404
|
+
# after the user clicks the "Continue" button.
|
|
405
|
+
# We make this happen by pushing this block on the head
|
|
406
|
+
# of the queue, but without any values - we don't want
|
|
407
|
+
# to trigger any param changes.
|
|
408
|
+
#
|
|
409
|
+
try:
|
|
410
|
+
dag.execute_after_input(w, dag_logger=dag_logger)
|
|
411
|
+
except BlockValidateError as e:
|
|
412
|
+
# Display the error as a notification.
|
|
413
|
+
#
|
|
414
|
+
block_name = html.escape(e.block_name)
|
|
415
|
+
error = html.escape(str(e))
|
|
416
|
+
notif = f'<b>{block_name}</b>:<br>{error}'
|
|
417
|
+
pn.state.notifications.error(notif, duration=0)
|
|
418
|
+
finally:
|
|
419
|
+
parent_template.main[0].loading = False
|
|
420
|
+
c_button = pn.widgets.Button(name=w.continue_label, button_type='primary', align='end')
|
|
421
|
+
c_button.on_click(on_continue)
|
|
422
|
+
|
|
423
|
+
row = [doc, c_button] if doc else [c_button]
|
|
424
|
+
w_ = pn.Column(
|
|
425
|
+
w,
|
|
426
|
+
pn.Row(*row),
|
|
427
|
+
sizing_mode='stretch_width'
|
|
428
|
+
)
|
|
429
|
+
elif doc:
|
|
430
|
+
w_ = pn.Column(w, doc)
|
|
431
|
+
else:
|
|
432
|
+
w_ = w
|
|
433
|
+
|
|
434
|
+
super().__init__(w_, *args, sizing_mode='stretch_width', **kwargs)
|
|
435
|
+
|
|
436
|
+
self.header = pn.Row(
|
|
437
|
+
name_text,
|
|
438
|
+
pn.VSpacer(),
|
|
439
|
+
spacer,
|
|
440
|
+
self._get_state_light(_get_state_color(w._block_state))
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Watch the block state so we can update the staus light.
|
|
444
|
+
#
|
|
445
|
+
w.param.watch_values(self.state_change, '_block_state')
|
|
446
|
+
|
|
447
|
+
def state_change(self, _block_state: BlockState):
|
|
448
|
+
"""Watcher for the block state.
|
|
449
|
+
|
|
450
|
+
Updates the state light.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
self.header[-1] = self._get_state_light(_get_state_color(_block_state))
|
|
454
|
+
|
|
455
|
+
def _sier2_label_formatter(pname: str):
|
|
456
|
+
"""Default formatter to turn parameter names into appropriate widget labels.
|
|
457
|
+
|
|
458
|
+
Make labels nicer for Panel.
|
|
459
|
+
|
|
460
|
+
Panel uses the label to display a caption for the corresponding input widgets.
|
|
461
|
+
The default label is taken from the name of the param, which means the default
|
|
462
|
+
caption starts with "In ".
|
|
463
|
+
|
|
464
|
+
Removes the "in_" prefix from input parameters, then passes the param name
|
|
465
|
+
to paramp.default_label_formatter.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
if pname.startswith('in_'):
|
|
469
|
+
pname = pname[3:]
|
|
470
|
+
|
|
471
|
+
return paramp.default_label_formatter(pname)
|
|
472
|
+
|
|
473
|
+
class PanelDag(Dag):
|
|
474
|
+
def __init__(self, *, site: str='Panel Dag', title: str, doc: str):
|
|
475
|
+
super().__init__(site=site, title=title, doc=doc)
|
|
476
|
+
paramp.label_formatter = _sier2_label_formatter
|
|
477
|
+
# self.template = _prepare_to_show(self)
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def template(self):
|
|
481
|
+
return _prepare_to_show(self)
|
|
482
|
+
|
|
483
|
+
def show(self):
|
|
484
|
+
pn.state.on_session_destroyed(_quit)
|
|
485
|
+
|
|
486
|
+
# Execute the dag.
|
|
487
|
+
# Since this is a panel dag, we expect the first block to be an input nlock.
|
|
488
|
+
# This ensures that the first block's prepare() method is called.
|
|
489
|
+
# If the first block is not an input block, it must be primed, just like a plain dag.
|
|
490
|
+
#
|
|
491
|
+
self.execute()
|
|
492
|
+
|
|
493
|
+
self.template.show(threaded=False)
|
|
494
|
+
|
|
495
|
+
def servable(self):
|
|
496
|
+
pn.state.on_session_destroyed(_quit)
|
|
497
|
+
|
|
498
|
+
# Execute the dag.
|
|
499
|
+
# Since this is a panel dag, we expect the first block to be an input nlock.
|
|
500
|
+
# This ensures that the first block's prepare() method is called.
|
|
501
|
+
# If the first block is not an input block, it must be primed, just like a plain dag.
|
|
502
|
+
#
|
|
503
|
+
self.execute()
|
|
504
|
+
|
|
505
|
+
self.template.servable()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .._block import BlockState
|
|
3
|
+
from .._util import trim
|
|
4
|
+
|
|
5
|
+
def _get_state_color(gs: BlockState) -> str:
|
|
6
|
+
"""Convert a block state (as logged by the dag) to a color.
|
|
7
|
+
|
|
8
|
+
The colors are arbitrary, except for BLOCK. When a block logs a message,
|
|
9
|
+
it is executing by definition, so no color is required.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
match gs:
|
|
13
|
+
case BlockState.BLOCK:
|
|
14
|
+
color = 'var(--panel-background-color)'
|
|
15
|
+
case BlockState.DAG:
|
|
16
|
+
color = 'grey'
|
|
17
|
+
case BlockState.INPUT:
|
|
18
|
+
color = '#f0c820'
|
|
19
|
+
case BlockState.READY:
|
|
20
|
+
color='white'
|
|
21
|
+
case BlockState.EXECUTING:
|
|
22
|
+
color='steelblue'
|
|
23
|
+
case BlockState.WAITING:
|
|
24
|
+
color='yellow'
|
|
25
|
+
case BlockState.SUCCESSFUL:
|
|
26
|
+
color = 'green'
|
|
27
|
+
case BlockState.INTERRUPTED:
|
|
28
|
+
color= 'orange'
|
|
29
|
+
case BlockState.ERROR:
|
|
30
|
+
color = 'red'
|
|
31
|
+
case _:
|
|
32
|
+
color = 'magenta'
|
|
33
|
+
|
|
34
|
+
return color
|
|
35
|
+
|
|
36
|
+
########
|
|
37
|
+
# Documentation utilities
|
|
38
|
+
########
|
|
39
|
+
|
|
40
|
+
def block_doc(block):
|
|
41
|
+
"""Generate Markdown documentation for a block.
|
|
42
|
+
|
|
43
|
+
The documentation is taken from the docstring of the block class
|
|
44
|
+
and the doc of each 'in_' and 'out_' param.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Force the first line of the block docstring to have a level 2 header.
|
|
48
|
+
#
|
|
49
|
+
b_doc = '## ' + trim(block.__doc__).lstrip(' #')
|
|
50
|
+
|
|
51
|
+
params = []
|
|
52
|
+
for name, p in block.param.objects().items():
|
|
53
|
+
if name.startswith(('in_', 'out_')):
|
|
54
|
+
doc = p.doc if p.doc else ''
|
|
55
|
+
params.append((name, doc.strip()))
|
|
56
|
+
|
|
57
|
+
params.sort()
|
|
58
|
+
text = ['| Name | Description |', '| ---- | ---- |']
|
|
59
|
+
for name, doc in params:
|
|
60
|
+
text.append(f'| {name} | {doc}')
|
|
61
|
+
|
|
62
|
+
return '---\n' + b_doc + '\n### Params\n' + '\n'.join(text)
|
|
63
|
+
|
|
64
|
+
def dag_doc(dag):
|
|
65
|
+
"""Generate Markdown documentation for a dag and its blocks."""
|
|
66
|
+
|
|
67
|
+
# A Block may be in the dag more than once.
|
|
68
|
+
# Don't include the documentation for such blocks more than once.
|
|
69
|
+
#
|
|
70
|
+
blocks = dag.get_sorted()
|
|
71
|
+
uniq_blocks = []
|
|
72
|
+
seen_blocks = set()
|
|
73
|
+
for b in blocks:
|
|
74
|
+
if type(b) not in seen_blocks:
|
|
75
|
+
uniq_blocks.append(b)
|
|
76
|
+
seen_blocks.add(type(b))
|
|
77
|
+
block_docs = '\n\n'.join(block_doc(block) for block in uniq_blocks)
|
|
78
|
+
|
|
79
|
+
# Force the first line of the dag doc to have a level 1 header.
|
|
80
|
+
#
|
|
81
|
+
dag_text =f'# {dag.site} - {dag.title}\n\n# ' + trim(dag.doc).lstrip(' #')
|
|
82
|
+
|
|
83
|
+
return f'{dag_text}\n\n{block_docs}'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sier2
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Blocks of code that are executed in dags
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: Algol60
|
|
7
|
+
Author-email: algol60 <algol60@users.noreply.github.com>
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Project-URL: Homepage, https://github.com/sier2/sier2
|
|
17
|
+
Description-Content-Type: text/x-rst
|
|
18
|
+
|
|
19
|
+
Sier2
|
|
20
|
+
======
|
|
21
|
+
|
|
22
|
+
Connect modular pieces of Python code ("blocks") into
|
|
23
|
+
a processing dag pipeline. Blocks are an improvement on libraries;
|
|
24
|
+
if you have a library, you still need to build an application.
|
|
25
|
+
Blocks are pieces of an application, you just have to connect them.
|
|
26
|
+
|
|
27
|
+
See the ``examples`` directory in the ``sier2-tutorial`` repository for examples.
|
|
28
|
+
|
|
29
|
+
Description
|
|
30
|
+
-----------
|
|
31
|
+
|
|
32
|
+
A ``block`` is a self-contained piece of code with input and output parameters.
|
|
33
|
+
Blocks can be connected to each other using a ``Dag`` to create
|
|
34
|
+
a dag of blocks.
|
|
35
|
+
|
|
36
|
+
More precisely, output parameters in one block can be connected to input parameters
|
|
37
|
+
in another block. The connections need not be one-to-one: parameters in multiple blocks
|
|
38
|
+
can be connected to parameters in a single block; conversely, parameters in a single block
|
|
39
|
+
can be connected to parameters in multiple blocks.
|
|
40
|
+
|
|
41
|
+
Block parameters use `param <https://param.holoviz.org/>`_, which not only implement
|
|
42
|
+
triggering and watching of events, but allow parameters to be named and documented.
|
|
43
|
+
|
|
44
|
+
A typical block implementation looks like this.
|
|
45
|
+
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
from sier2 import Block
|
|
49
|
+
|
|
50
|
+
class Increment(Block):
|
|
51
|
+
"""A block that adds one to the input value."""
|
|
52
|
+
|
|
53
|
+
in_int = param.Integer(label='The input', doc='An integer')
|
|
54
|
+
out_int = param.Integer(label='The output', doc='The incremented value')
|
|
55
|
+
|
|
56
|
+
def execute(self):
|
|
57
|
+
self.out_int = self.in_int + 1
|
|
58
|
+
|
|
59
|
+
See the examples in ``examples`` (Python scripts) and ``examples-panel`` (scripts that use `Panel <https://panel.holoviz.org/>`_ as a UI).
|
|
60
|
+
|
|
61
|
+
Documentation
|
|
62
|
+
-------------
|
|
63
|
+
|
|
64
|
+
To build the documentation from the repository root directory:
|
|
65
|
+
|
|
66
|
+
.. code-block:: powershell
|
|
67
|
+
|
|
68
|
+
docs/make html
|
|
69
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
sier2/__init__.py,sha256=lAZfqYn8xu4JO7Klb4RvY8YbZTLP4_3OGK6FbUpzsPc,238
|
|
2
|
+
sier2/__main__.py,sha256=C989H9-UP176mKEUbNZcnn95iHd0SzjrGFermpau4qs,3828
|
|
3
|
+
sier2/_block.py,sha256=ofwOvoVnHAj9r3AFJw-t4XrycQurQTjOOE8Ta5qtd1I,10460
|
|
4
|
+
sier2/_config.py,sha256=-vWD8CQv2PIDoIeXaJtQuObr9l6JNwoVAWGh2XrMZY8,8297
|
|
5
|
+
sier2/_dag.py,sha256=jZS4aTf0wXWqD9QDHw_D8StcESDRYjPrRO91UhiKthQ,22353
|
|
6
|
+
sier2/_library.py,sha256=xPxQqTUKgRUBaYwfNAo0320oGbKKiiaErLl5NeKCfqY,8210
|
|
7
|
+
sier2/_logger.py,sha256=lwRmYjXMkP7TyURo5gvOf266vwGDFkLGau-EXpo5eEw,2379
|
|
8
|
+
sier2/_util.py,sha256=iWsce1qCI8ySpWMc75Buec5_AB5AuhLcUweW4vznwjE,4002
|
|
9
|
+
sier2/_version.py,sha256=K5EdVMOTOHqhr-mIMjXhh84WHTSES2K-MJ_b--KryBM,71
|
|
10
|
+
sier2/panel/__init__.py,sha256=wDEf_v859flQX4udAVYZW1m79sfB1NIrI3pyNIpNiEM,29
|
|
11
|
+
sier2/panel/_chart.py,sha256=CBqkwoyFC4XEDUTTRvZn8lfiC4tZ1kLFGkAXJMYpD-E,10059
|
|
12
|
+
sier2/panel/_feedlogger.py,sha256=tsrA8R2FZUecVY2egifVu2qosRfjccgvGRE0lLZSXZY,5270
|
|
13
|
+
sier2/panel/_panel.py,sha256=4_4NxPAa8xTtJOpkylVptiEGbnIP67V3L_6oH6qn6hs,17456
|
|
14
|
+
sier2/panel/_panel_util.py,sha256=omcLO0OIHhH00l9YXv09Qv8lnaY6VKsQ1F0qbsrs3vk,2450
|
|
15
|
+
sier2-1.0.1.dist-info/METADATA,sha256=7-dP8tAs6E0dilntUR6wok2RodLBXZgGG8I89hRAqSg,2390
|
|
16
|
+
sier2-1.0.1.dist-info/WHEEL,sha256=MICUlqIgkuEnKh9OWy254Ca7q2MHOW-q0u36TZR60nU,92
|
|
17
|
+
sier2-1.0.1.dist-info/licenses/LICENSE,sha256=2AKq0yxLLDdGsj6xQuNjDPG5d2IbFWFGiB_cnCBtMp4,1064
|
|
18
|
+
sier2-1.0.1.dist-info/RECORD,,
|