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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py2.py3-none-any