sier2 0.29__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.
Potentially problematic release.
This version of sier2 might be problematic. Click here for more details.
- sier2/__init__.py +4 -0
- sier2/__main__.py +96 -0
- sier2/_block.py +221 -0
- sier2/_dag.py +694 -0
- sier2/_library.py +243 -0
- sier2/_logger.py +65 -0
- sier2/_util.py +65 -0
- sier2/_version.py +3 -0
- sier2/panel/__init__.py +1 -0
- sier2/panel/_feedlogger.py +153 -0
- sier2/panel/_panel.py +412 -0
- sier2/panel/_panel_util.py +83 -0
- sier2-0.29.dist-info/LICENSE +21 -0
- sier2-0.29.dist-info/METADATA +68 -0
- sier2-0.29.dist-info/RECORD +16 -0
- sier2-0.29.dist-info/WHEEL +4 -0
sier2/panel/_panel.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import html
|
|
4
|
+
import panel as pn
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from sier2 import Block, BlockValidateError, BlockState, Dag, BlockError
|
|
10
|
+
from .._dag import _InputValues
|
|
11
|
+
from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
|
|
12
|
+
from ._panel_util import _get_state_color, dag_doc
|
|
13
|
+
|
|
14
|
+
NTHREADS = 2
|
|
15
|
+
|
|
16
|
+
# From https://tabler.io/icons/icon/info-circle
|
|
17
|
+
#
|
|
18
|
+
INFO_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
19
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
20
|
+
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
|
21
|
+
<path d="M12 9h.01" />
|
|
22
|
+
<path d="M11 12h1v4h1" />
|
|
23
|
+
</svg>
|
|
24
|
+
'''
|
|
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
|
+
|
|
46
|
+
|
|
47
|
+
def _hms(sec):
|
|
48
|
+
h, sec = divmod(int(sec), 3600)
|
|
49
|
+
m, sec = divmod(sec, 60)
|
|
50
|
+
|
|
51
|
+
return f'{h:02}:{m:02}:{sec:02}'
|
|
52
|
+
|
|
53
|
+
class _PanelContext:
|
|
54
|
+
"""A context manager to wrap the execution of a block within a dag.
|
|
55
|
+
|
|
56
|
+
This default context manager handles the block state, the stopper,
|
|
57
|
+
and converts block execution errors to GimzoError exceptions.
|
|
58
|
+
|
|
59
|
+
It also uses the panel UI to provide extra information to the user.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, *, block: Block, dag: Dag, dag_logger=None):
|
|
63
|
+
self.block = block
|
|
64
|
+
self.dag = dag
|
|
65
|
+
self.dag_logger = dag_logger
|
|
66
|
+
|
|
67
|
+
def __enter__(self):
|
|
68
|
+
state = BlockState.EXECUTING
|
|
69
|
+
self.block._block_state = state
|
|
70
|
+
self.t0 = datetime.now()
|
|
71
|
+
if self.dag_logger:
|
|
72
|
+
self.dag_logger.info('Execute', block_name=self.block.name, block_state=state)
|
|
73
|
+
|
|
74
|
+
block_logger = getBlockPanelLogger(self.block.name)
|
|
75
|
+
self.block.logger = block_logger
|
|
76
|
+
|
|
77
|
+
# if self.block._progress:
|
|
78
|
+
# self.block._progress.active = True
|
|
79
|
+
|
|
80
|
+
return self.block
|
|
81
|
+
|
|
82
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
83
|
+
delta = (datetime.now() - self.t0).total_seconds()
|
|
84
|
+
|
|
85
|
+
# if self.block._progress:
|
|
86
|
+
# self.block._progress.active = False
|
|
87
|
+
|
|
88
|
+
if exc_type is None:
|
|
89
|
+
state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
|
|
90
|
+
self.block._block_state = state
|
|
91
|
+
if self.dag_logger:
|
|
92
|
+
self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
|
|
93
|
+
elif isinstance(exc_type, KeyboardInterrupt):
|
|
94
|
+
state = BlockState.INTERRUPTED
|
|
95
|
+
self.block_state._block_state = state
|
|
96
|
+
if not self.dag._is_pyodide:
|
|
97
|
+
self.dag._stopper.event.set()
|
|
98
|
+
if self.dag_logger:
|
|
99
|
+
self.dag_logger.exception(f'KEYBOARD INTERRUPT after {_hms(delta)}', block_name=self.block.name, block_state=state)
|
|
100
|
+
else:
|
|
101
|
+
state = BlockState.ERROR
|
|
102
|
+
self.block._block_state = state
|
|
103
|
+
if exc_type is not BlockValidateError:
|
|
104
|
+
if self.dag_logger:
|
|
105
|
+
self.dag_logger.exception(
|
|
106
|
+
f'after {_hms(delta)}',
|
|
107
|
+
block_name=self.block.name,
|
|
108
|
+
block_state=state
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# msg = f'While in {self.block.name}.execute(): {exc_val}'
|
|
112
|
+
if not self.dag._is_pyodide:
|
|
113
|
+
self.dag._stopper.event.set()
|
|
114
|
+
|
|
115
|
+
if not issubclass(exc_type, BlockError):
|
|
116
|
+
# Convert the error in the block to a BlockError.
|
|
117
|
+
#
|
|
118
|
+
raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
def _quit(session_context):
|
|
122
|
+
print(session_context)
|
|
123
|
+
sys.exit()
|
|
124
|
+
|
|
125
|
+
def interrupt_thread(tid, exctype):
|
|
126
|
+
"""Raise exception exctype in thread tid."""
|
|
127
|
+
|
|
128
|
+
r = ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
129
|
+
ctypes.c_ulong(tid),
|
|
130
|
+
ctypes.py_object(exctype)
|
|
131
|
+
)
|
|
132
|
+
if r==0:
|
|
133
|
+
raise ValueError('Invalid thread id')
|
|
134
|
+
elif r!=1:
|
|
135
|
+
# "if it returns a number greater than one, you're in trouble,
|
|
136
|
+
# and you should call it again with exc=NULL to revert the effect"
|
|
137
|
+
#
|
|
138
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
|
|
139
|
+
raise SystemError('PyThreadState_SetAsyncExc failed')
|
|
140
|
+
|
|
141
|
+
def _prepare_to_show(dag: Dag):
|
|
142
|
+
# Replace the default text-based context with the panel-based context.
|
|
143
|
+
#
|
|
144
|
+
dag._block_context = _PanelContext
|
|
145
|
+
|
|
146
|
+
info_button = pn.widgets.ButtonIcon(
|
|
147
|
+
icon=INFO_SVG,
|
|
148
|
+
active_icon=INFO_SVG,
|
|
149
|
+
description='Dag Help',
|
|
150
|
+
align='center'
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# A place to stash the info FloatPanel.
|
|
154
|
+
#
|
|
155
|
+
info_fp_holder = pn.Column(visible=False)
|
|
156
|
+
|
|
157
|
+
sidebar_title = pn.Row(info_button, '## Blocks')
|
|
158
|
+
template = pn.template.BootstrapTemplate(
|
|
159
|
+
site=dag.site,
|
|
160
|
+
title=dag.title,
|
|
161
|
+
theme='dark',
|
|
162
|
+
sidebar=pn.Column(sidebar_title),
|
|
163
|
+
collapsed_sidebar=True,
|
|
164
|
+
sidebar_width=440
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def display_info(_event):
|
|
168
|
+
"""Display a FloatPanel containing help for the dag and blocks."""
|
|
169
|
+
|
|
170
|
+
text = dag_doc(dag)
|
|
171
|
+
config = {
|
|
172
|
+
'headerControls': {'maximize': 'remove'},
|
|
173
|
+
'contentOverflow': 'scroll'
|
|
174
|
+
}
|
|
175
|
+
fp = pn.layout.FloatPanel(text, name=dag.title, width=550, height=450, contained=False, position='center', theme='dark filleddark', config=config)
|
|
176
|
+
info_fp_holder[:] = [fp]
|
|
177
|
+
|
|
178
|
+
info_button.on_click(display_info)
|
|
179
|
+
|
|
180
|
+
switch = pn.widgets.Switch(name='Stop')
|
|
181
|
+
|
|
182
|
+
def on_switch(event):
|
|
183
|
+
if switch.value:
|
|
184
|
+
dag.stop()
|
|
185
|
+
reset()
|
|
186
|
+
|
|
187
|
+
# Which thread are we running on?
|
|
188
|
+
#
|
|
189
|
+
current_tid = threading.current_thread().ident
|
|
190
|
+
|
|
191
|
+
# What other threads are running?
|
|
192
|
+
# There are multiple threads running, including the main thread
|
|
193
|
+
# and the bokeh server thread. We need to find the panel threads.
|
|
194
|
+
# Unfortunately, there is nothing special about them.
|
|
195
|
+
#
|
|
196
|
+
print('THREADS', current_tid, [t for t in threading.enumerate()])
|
|
197
|
+
all_threads = [t for t in threading.enumerate() if t.name.startswith('ThreadPoolExecutor')]
|
|
198
|
+
assert len(all_threads)<=NTHREADS, f'{all_threads=}'
|
|
199
|
+
other_thread = [t for t in all_threads if t.ident!=current_tid]
|
|
200
|
+
|
|
201
|
+
# It's possible that since the user might not have done anything yet,
|
|
202
|
+
# another thread hasn't spun up.
|
|
203
|
+
#
|
|
204
|
+
if other_thread:
|
|
205
|
+
interrupt_thread(other_thread[0].ident, KeyboardInterrupt)
|
|
206
|
+
else:
|
|
207
|
+
dag.unstop()
|
|
208
|
+
# TODO reset status for each card
|
|
209
|
+
|
|
210
|
+
pn.bind(on_switch, switch, watch=True)
|
|
211
|
+
|
|
212
|
+
def reset():
|
|
213
|
+
"""Experiment."""
|
|
214
|
+
col = template.main.objects[0]
|
|
215
|
+
for card in col:
|
|
216
|
+
status = card.header[0]
|
|
217
|
+
|
|
218
|
+
# We use a Panel Feed widget to display log messages.
|
|
219
|
+
#
|
|
220
|
+
log_feed = pn.Feed(
|
|
221
|
+
view_latest=True,
|
|
222
|
+
scroll_button_threshold=20,
|
|
223
|
+
auto_scroll_limit=1,
|
|
224
|
+
sizing_mode='stretch_width'
|
|
225
|
+
)
|
|
226
|
+
dag_logger = getDagPanelLogger(log_feed)
|
|
227
|
+
|
|
228
|
+
template.main.append(
|
|
229
|
+
pn.Column(
|
|
230
|
+
*(BlockCard(parent_template=template, dag=dag, w=gw, dag_logger=dag_logger) for gw in dag.get_sorted())
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
template.sidebar.append(
|
|
234
|
+
pn.Column(
|
|
235
|
+
switch,
|
|
236
|
+
pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
|
|
237
|
+
log_feed,
|
|
238
|
+
info_fp_holder
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return template
|
|
243
|
+
|
|
244
|
+
def _show_dag(dag: Dag):
|
|
245
|
+
"""Display the dag in a panel template."""
|
|
246
|
+
|
|
247
|
+
template = _prepare_to_show(dag)
|
|
248
|
+
|
|
249
|
+
pn.state.on_session_destroyed(_quit)
|
|
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
|
+
|
|
258
|
+
template.show(threaded=False)
|
|
259
|
+
|
|
260
|
+
def _serveable_dag(dag: Dag):
|
|
261
|
+
"""Serve the dag in a panel template."""
|
|
262
|
+
|
|
263
|
+
template = _prepare_to_show(dag)
|
|
264
|
+
|
|
265
|
+
pn.state.on_session_destroyed(_quit)
|
|
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
|
+
|
|
274
|
+
template.servable()
|
|
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
|
+
|
|
286
|
+
class BlockCard(pn.Card):
|
|
287
|
+
"""A custom card to wrap around a block.
|
|
288
|
+
|
|
289
|
+
This adds the block title and a status light to the card header.
|
|
290
|
+
The light updates to match the block state.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _get_state_light(color: str) -> pn.Spacer:
|
|
295
|
+
return pn.Spacer(
|
|
296
|
+
margin=(8, 0, 0, 0),
|
|
297
|
+
styles={'width':'20px', 'height':'20px', 'background':color, 'border-radius': '10px'}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# def ui(self, message):
|
|
301
|
+
# """TODO connect this to the template"""
|
|
302
|
+
# print(message)
|
|
303
|
+
|
|
304
|
+
def __init__(self, *args, parent_template, dag: Dag, w: Block, dag_logger=None, **kwargs):
|
|
305
|
+
# Make this look like <h3> (the default Card header text).
|
|
306
|
+
#
|
|
307
|
+
name_text = pn.widgets.StaticText(
|
|
308
|
+
value=w.name,
|
|
309
|
+
css_classes=['card-title'],
|
|
310
|
+
styles={'font-size':'1.17em', 'font-weight':'bold'}
|
|
311
|
+
)
|
|
312
|
+
spacer = pn.HSpacer(
|
|
313
|
+
styles=dict(
|
|
314
|
+
min_width='1px', min_height='1px'
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# If a block has no __panel__() method, Panel will by default
|
|
319
|
+
# inspect the class and display the param attributes.
|
|
320
|
+
# This is obviously not what we want.
|
|
321
|
+
#
|
|
322
|
+
# We just want to display the in_ params.
|
|
323
|
+
#
|
|
324
|
+
has_panel = '__panel__' in w.__class__.__dict__
|
|
325
|
+
if not has_panel:
|
|
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)
|
|
336
|
+
|
|
337
|
+
if w.block_pause_execution:
|
|
338
|
+
# This is an input block, so add a 'Continue' button.
|
|
339
|
+
#
|
|
340
|
+
def on_continue(_event):
|
|
341
|
+
# The user may not have changed anything from the default values,
|
|
342
|
+
# so there won't be anything on the block queue.
|
|
343
|
+
# Therefore, we trigger the output params to put their
|
|
344
|
+
# current values on the queue.
|
|
345
|
+
# If their values are already there, it doesn't matter.
|
|
346
|
+
#
|
|
347
|
+
parent_template.main[0].loading = True
|
|
348
|
+
w.param.trigger(*w._block_out_params)
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
if dag_logger:
|
|
352
|
+
dag_logger.info('', block_name=None, block_state=None)
|
|
353
|
+
dag_logger.info('Execute dag', block_name='', block_state=BlockState.DAG)
|
|
354
|
+
|
|
355
|
+
# We want this block's execute() method to run first
|
|
356
|
+
# after the user clicks the "Continue" button.
|
|
357
|
+
# We make this happen by pushing this block on the head
|
|
358
|
+
# of the queue, but without any values - we don't want
|
|
359
|
+
# to trigger any param changes.
|
|
360
|
+
#
|
|
361
|
+
try:
|
|
362
|
+
dag.execute_after_input(w, dag_logger=dag_logger)
|
|
363
|
+
except BlockValidateError as e:
|
|
364
|
+
# Display the error as a notification.
|
|
365
|
+
#
|
|
366
|
+
block_name = html.escape(e.block_name)
|
|
367
|
+
error = html.escape(str(e))
|
|
368
|
+
notif = f'<b>{block_name}</b>:<br>{error}'
|
|
369
|
+
pn.state.notifications.error(notif, duration=0)
|
|
370
|
+
finally:
|
|
371
|
+
parent_template.main[0].loading = False
|
|
372
|
+
c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
|
|
373
|
+
c_button.on_click(on_continue)
|
|
374
|
+
|
|
375
|
+
w_ = pn.Column(
|
|
376
|
+
w,
|
|
377
|
+
pn.Row(c_button, align='end'),
|
|
378
|
+
sizing_mode='scale_width'
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
w_ = w
|
|
382
|
+
|
|
383
|
+
super().__init__(w_, *args, sizing_mode='stretch_width', **kwargs)
|
|
384
|
+
|
|
385
|
+
self.header = pn.Row(
|
|
386
|
+
name_text,
|
|
387
|
+
pn.VSpacer(),
|
|
388
|
+
spacer,
|
|
389
|
+
self._get_state_light(_get_state_color(w._block_state))
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Watch the block state so we can update the staus light.
|
|
393
|
+
#
|
|
394
|
+
w.param.watch_values(self.state_change, '_block_state')
|
|
395
|
+
|
|
396
|
+
def state_change(self, _block_state: BlockState):
|
|
397
|
+
"""Watcher for the block state.
|
|
398
|
+
|
|
399
|
+
Updates the state light.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
self.header[-1] = self._get_state_light(_get_state_color(_block_state))
|
|
403
|
+
|
|
404
|
+
class PanelDag(Dag):
|
|
405
|
+
def __init__(self, *, site: str='Panel Dag', title: str, doc: str):
|
|
406
|
+
super().__init__(site=site, title=title, doc=doc)
|
|
407
|
+
|
|
408
|
+
def show(self):
|
|
409
|
+
_show_dag(self)
|
|
410
|
+
|
|
411
|
+
def servable(self):
|
|
412
|
+
_serveable_dag(self)
|
|
@@ -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,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Algol60
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sier2
|
|
3
|
+
Version: 0.29
|
|
4
|
+
Summary: Blocks of code that are executed in dags
|
|
5
|
+
Author: Algol60
|
|
6
|
+
Author-email: algol60 <algol60@users.noreply.github.com>
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Project-URL: Homepage, https://github.com/algol60/sier2
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
|
|
18
|
+
Sier2
|
|
19
|
+
======
|
|
20
|
+
|
|
21
|
+
Connect modular pieces of Python code ("blocks") into
|
|
22
|
+
a processing dag pipeline. Blocks are an improvement on libraries;
|
|
23
|
+
if you have a library, you still need to build an application.
|
|
24
|
+
Blocks are pieces of an application, you just have to connect them.
|
|
25
|
+
|
|
26
|
+
See the ``examples`` directory for examples.
|
|
27
|
+
|
|
28
|
+
Description
|
|
29
|
+
-----------
|
|
30
|
+
|
|
31
|
+
A ``block`` is a self-contained piece of code with input and output parameters.
|
|
32
|
+
Blocks can be connected to each other using a ``Dag`` to create
|
|
33
|
+
a dag of blocks.
|
|
34
|
+
|
|
35
|
+
More precisely, output parameters in one block can be connected to input parameters
|
|
36
|
+
in another block. The connections need not be one-to-one: parameters in multiple blocks
|
|
37
|
+
can be connected to parameters in a single block; conversely, parameters in a single block
|
|
38
|
+
can be connected to parameters in multiple blocks.
|
|
39
|
+
|
|
40
|
+
Block parameters use `param <https://param.holoviz.org/>`_, which not only implement
|
|
41
|
+
triggering and watching of events, but allow parameters to be named and documented.
|
|
42
|
+
|
|
43
|
+
A typical block implementation looks like this.
|
|
44
|
+
|
|
45
|
+
.. code-block:: python
|
|
46
|
+
|
|
47
|
+
from sier2 import Block
|
|
48
|
+
|
|
49
|
+
class Increment(Block):
|
|
50
|
+
"""A block that adds one to the input value."""
|
|
51
|
+
|
|
52
|
+
int_in = param.Integer(label='The input', doc='An integer')
|
|
53
|
+
int_out = param.Integer(label='The output', doc='The incremented value')
|
|
54
|
+
|
|
55
|
+
def execute(self):
|
|
56
|
+
self.int_out = self.int_in + 1
|
|
57
|
+
|
|
58
|
+
See the examples in ``examples`` (Python scripts) and ``examples-panel`` (scripts that use `Panel <https://panel.holoviz.org/>`_ as a UI).
|
|
59
|
+
|
|
60
|
+
Documentation
|
|
61
|
+
-------------
|
|
62
|
+
|
|
63
|
+
To build the documentation from the repository root directory:
|
|
64
|
+
|
|
65
|
+
.. code-block:: powershell
|
|
66
|
+
|
|
67
|
+
docs/make html
|
|
68
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
sier2/__init__.py,sha256=_JNRxOnwTgKfuaNEF1P9iRgBNX2P8i-GuibUG0KVdS8,174
|
|
2
|
+
sier2/__main__.py,sha256=HZfzJLaD2_JOyKFkFYTD2vs-UARxNMjP4D7ZdJg405A,3140
|
|
3
|
+
sier2/_block.py,sha256=_0aBr84mikRoP_FfEcpg92JTqOdoB9kObu6Y0u8KeKc,7716
|
|
4
|
+
sier2/_dag.py,sha256=BPB22UEScfS1lYL8fzpGTx7IOqvnyTcFsz9QgFI4Fn4,23976
|
|
5
|
+
sier2/_library.py,sha256=BGEG78AH16IAhI7NEDL-3CrEwNYXFUo4K-XFfHgKn70,8011
|
|
6
|
+
sier2/_logger.py,sha256=lwRmYjXMkP7TyURo5gvOf266vwGDFkLGau-EXpo5eEw,2379
|
|
7
|
+
sier2/_util.py,sha256=NmXI7QMSdkoSMe6EYJ-q8zI9iGJeMUto3g4314UVoM8,1932
|
|
8
|
+
sier2/_version.py,sha256=K5EdVMOTOHqhr-mIMjXhh84WHTSES2K-MJ_b--KryBM,71
|
|
9
|
+
sier2/panel/__init__.py,sha256=wDEf_v859flQX4udAVYZW1m79sfB1NIrI3pyNIpNiEM,29
|
|
10
|
+
sier2/panel/_feedlogger.py,sha256=tsrA8R2FZUecVY2egifVu2qosRfjccgvGRE0lLZSXZY,5270
|
|
11
|
+
sier2/panel/_panel.py,sha256=_0BZRebzFFTnyX-Qr4pgbAbf09QzKXSsZG6JFU1DOWI,13913
|
|
12
|
+
sier2/panel/_panel_util.py,sha256=omcLO0OIHhH00l9YXv09Qv8lnaY6VKsQ1F0qbsrs3vk,2450
|
|
13
|
+
sier2-0.29.dist-info/LICENSE,sha256=2AKq0yxLLDdGsj6xQuNjDPG5d2IbFWFGiB_cnCBtMp4,1064
|
|
14
|
+
sier2-0.29.dist-info/METADATA,sha256=ARQRzlQc-1_QSNseo6sgQ1da5uufFWPeQtm0AWolrqo,2332
|
|
15
|
+
sier2-0.29.dist-info/WHEEL,sha256=aiTauIPAnqOMBuVimVqk4bevIILHLWXNdkzTocSR-tg,92
|
|
16
|
+
sier2-0.29.dist-info/RECORD,,
|