sier2 0.17__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/panel/_panel.py ADDED
@@ -0,0 +1,320 @@
1
+ import ctypes
2
+ from datetime import datetime
3
+ import panel as pn
4
+ import sys
5
+ import threading
6
+
7
+ from sier2 import Block, BlockState, Dag, BlockError
8
+ from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
9
+ from ._panel_util import _get_state_color, dag_doc
10
+
11
+ NTHREADS = 2
12
+
13
+ # From https://tabler.io/icons/icon/info-circle
14
+ #
15
+ 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">
16
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
17
+ <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
18
+ <path d="M12 9h.01" />
19
+ <path d="M11 12h1v4h1" />
20
+ </svg>
21
+ '''
22
+
23
+ pn.extension('floatpanel', inline=True, nthreads=NTHREADS, loading_spinner='bar')
24
+
25
+ def _hms(sec):
26
+ h, sec = divmod(int(sec), 3600)
27
+ m, sec = divmod(sec, 60)
28
+
29
+ return f'{h:02}:{m:02}:{sec:02}'
30
+
31
+ class _PanelContext:
32
+ """A context manager to wrap the execution of a block within a dag.
33
+
34
+ This default context manager handles the block state, the stopper,
35
+ and converts block execution errors to GimzoError exceptions.
36
+
37
+ It also uses the panel UI to provide extra information to the user.
38
+ """
39
+
40
+ def __init__(self, *, block: Block, dag: Dag, dag_logger=None):
41
+ self.block = block
42
+ self.dag = dag
43
+ self.dag_logger = dag_logger
44
+
45
+ def __enter__(self):
46
+ state = BlockState.EXECUTING
47
+ self.block._block_state = state
48
+ self.t0 = datetime.now()
49
+ if self.dag_logger:
50
+ self.dag_logger.info('Execute', block_name=self.block.name, block_state=state)
51
+
52
+ block_logger = getBlockPanelLogger(self.block.name)
53
+ self.block.logger = block_logger
54
+
55
+ if self.block._progress:
56
+ self.block._progress.active = True
57
+
58
+ return self.block
59
+
60
+ def __exit__(self, exc_type, exc_val, exc_tb):
61
+ delta = (datetime.now() - self.t0).total_seconds()
62
+
63
+ if self.block._progress:
64
+ self.block._progress.active = False
65
+
66
+ if exc_type is None:
67
+ state = BlockState.WAITING if self.block.user_input else BlockState.SUCCESSFUL
68
+ self.block._block_state = state
69
+ if self.dag_logger:
70
+ self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
71
+ elif isinstance(exc_type, KeyboardInterrupt):
72
+ state = BlockState.INTERRUPTED
73
+ self.block_state._block_state = state
74
+ self.dag._stopper.event.set()
75
+ if self.dag_logger:
76
+ self.dag_logger.exception(f'KEYBOARD INTERRUPT after {_hms(delta)}', block_name=self.block.name, block_state=state)
77
+ else:
78
+ state = BlockState.ERROR
79
+ 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
88
+
89
+ return False
90
+
91
+ def _quit(session_context):
92
+ print(session_context)
93
+ sys.exit()
94
+
95
+ def interrupt_thread(tid, exctype):
96
+ """Raise exception exctype in thread tid."""
97
+
98
+ r = ctypes.pythonapi.PyThreadState_SetAsyncExc(
99
+ ctypes.c_ulong(tid),
100
+ ctypes.py_object(exctype)
101
+ )
102
+ if r==0:
103
+ raise ValueError('Invalid thread id')
104
+ elif r!=1:
105
+ # "if it returns a number greater than one, you're in trouble,
106
+ # and you should call it again with exc=NULL to revert the effect"
107
+ #
108
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
109
+ raise SystemError('PyThreadState_SetAsyncExc failed')
110
+
111
+ def _show_dag(dag: Dag):
112
+ """Display the dag in a panel template."""
113
+
114
+ # Replace the default text-based context with the panel-based context.
115
+ #
116
+ dag._block_context = _PanelContext
117
+
118
+ info_button = pn.widgets.ButtonIcon(
119
+ icon=INFO_SVG,
120
+ active_icon=INFO_SVG,
121
+ description='Dag Help',
122
+ align='center'
123
+ )
124
+
125
+ fp_holder = pn.Column(visible=False)
126
+
127
+ sidebar_title = pn.Row(info_button, '## Blocks')
128
+ template = pn.template.BootstrapTemplate(
129
+ site=dag.site,
130
+ title=dag.title,
131
+ theme='dark',
132
+ sidebar=pn.Column(sidebar_title),
133
+ collapsed_sidebar=True,
134
+ sidebar_width=440
135
+ )
136
+
137
+ def display_info(_event):
138
+ """Display a FloatPanel containing help for the dag and blocks."""
139
+
140
+ text = dag_doc(dag)
141
+ config = {
142
+ 'headerControls': {'maximize': 'remove'},
143
+ 'contentOverflow': 'scroll'
144
+ }
145
+ 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]
147
+ info_button.on_click(display_info)
148
+
149
+ switch = pn.widgets.Switch(name='Stop')
150
+
151
+ def on_switch(event):
152
+ if switch.value:
153
+ dag.stop()
154
+ reset()
155
+
156
+ # Which thread are we running on?
157
+ #
158
+ current_tid = threading.current_thread().ident
159
+
160
+ # What other threads are running?
161
+ # There are multiple threads running, including the main thread
162
+ # and the bokeh server thread. We need to find the panel threads.
163
+ # Unfortunately, there is nothing special about them.
164
+ #
165
+ print('THREADS', current_tid, [t for t in threading.enumerate()])
166
+ all_threads = [t for t in threading.enumerate() if t.name.startswith('ThreadPoolExecutor')]
167
+ assert len(all_threads)<=NTHREADS, f'{all_threads=}'
168
+ other_thread = [t for t in all_threads if t.ident!=current_tid]
169
+
170
+ # It's possible that since the user might not have done anything yet,
171
+ # another thread hasn't spun up.
172
+ #
173
+ if other_thread:
174
+ interrupt_thread(other_thread[0].ident, KeyboardInterrupt)
175
+ else:
176
+ dag.unstop()
177
+ # TODO reset status for each card
178
+
179
+ pn.bind(on_switch, switch, watch=True)
180
+
181
+ def reset():
182
+ """Experiment."""
183
+ col = template.main.objects[0]
184
+ for card in col:
185
+ status = card.header[0]
186
+
187
+ # We use a Panel Feed widget to display log messages.
188
+ #
189
+ log_feed = pn.Feed(
190
+ view_latest=True,
191
+ scroll_button_threshold=20,
192
+ auto_scroll_limit=1,
193
+ sizing_mode='stretch_width'
194
+ )
195
+ dag_logger = getDagPanelLogger(log_feed)
196
+
197
+ template.main.append(
198
+ pn.Column(
199
+ *(BlockCard(parent_template=template, dag=dag, w=gw, dag_logger=dag_logger) for gw in dag.get_sorted())
200
+ )
201
+ )
202
+ template.sidebar.append(
203
+ pn.Column(
204
+ switch,
205
+ pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
206
+ log_feed,
207
+ fp_holder
208
+ )
209
+ )
210
+
211
+ pn.state.on_session_destroyed(_quit)
212
+
213
+ template.show(threaded=False)
214
+
215
+ class BlockCard(pn.Card):
216
+ """A custom card to wrap around a block.
217
+
218
+ This adds the block title and a status light to the card header.
219
+ The light updates to match the block state.
220
+ """
221
+
222
+ @staticmethod
223
+ def _get_state_light(color: str) -> pn.Spacer:
224
+ return pn.Spacer(
225
+ margin=(8, 0, 0, 0),
226
+ styles={'width':'20px', 'height':'20px', 'background':color, 'border-radius': '10px'}
227
+ )
228
+
229
+ # def ui(self, message):
230
+ # """TODO connect this to the template"""
231
+ # print(message)
232
+
233
+ def __init__(self, *args, parent_template, dag: Dag, w: Block, dag_logger=None, **kwargs):
234
+ # Make this look like <h3> (the default Card header text).
235
+ #
236
+ name_text = pn.widgets.StaticText(
237
+ value=w.name,
238
+ css_classes=['card-title'],
239
+ styles={'font-size':'1.17em', 'font-weight':'bold'}
240
+ )
241
+ spacer = pn.HSpacer(
242
+ styles=dict(
243
+ min_width='1px', min_height='1px'
244
+ )
245
+ )
246
+
247
+ # If a block has no __panel__() method, Panel will by default
248
+ # inspect the class and display the param attributes.
249
+ # This is obviously not what we want.
250
+ #
251
+ # Instead, we want to display an indefinite progress bar.
252
+ # The Panel context manager will activate and deactivate it.
253
+ #
254
+ has_panel = '__panel__' in w.__class__.__dict__
255
+ if not has_panel:
256
+ w._progress = pn.indicators.Progress(
257
+ name='Block progress',
258
+ bar_color='primary',
259
+ active=False,
260
+ value=-1
261
+ )
262
+
263
+ if w.user_input:
264
+ # This is a user_input block, so add a 'Continue' button.
265
+ #
266
+ def on_continue(_event):
267
+ # The user may not have changed anything from the default values,
268
+ # so there won't be anything on the block queue.
269
+ # Therefore, we trigger the output params to put their
270
+ # current values on the queue.
271
+ # If their values are already there, it doesn't matter.
272
+ #
273
+ w.param.trigger(*w._block_out_params)
274
+ parent_template.main[0].loading = True
275
+ try:
276
+ if dag_logger:
277
+ dag_logger.info('', block_name=None, block_state=None)
278
+ dag_logger.info('Execute dag', block_name='', block_state=BlockState.DAG)
279
+ dag.execute(dag_logger=dag_logger)
280
+ finally:
281
+ parent_template.main[0].loading = False
282
+
283
+ c_button = pn.widgets.Button(name='Continue', button_type='primary')
284
+ pn.bind(on_continue, c_button, watch=True)
285
+
286
+ w_ = pn.Column(
287
+ w,
288
+ pn.Row(c_button, align='end'),
289
+ sizing_mode='scale_width'
290
+ )
291
+ else:
292
+ w_ = w
293
+
294
+ super().__init__(w_, *args, sizing_mode='stretch_width', **kwargs)
295
+
296
+ self.header = pn.Row(
297
+ name_text,
298
+ pn.VSpacer(),
299
+ spacer,
300
+ self._get_state_light(_get_state_color(w._block_state))
301
+ )
302
+
303
+ # Watch the block state so we can update the staus light.
304
+ #
305
+ w.param.watch_values(self.state_change, '_block_state')
306
+
307
+ def state_change(self, _block_state: BlockState):
308
+ """Watcher for the block state.
309
+
310
+ Updates the state light.
311
+ """
312
+
313
+ self.header[-1] = self._get_state_light(_get_state_color(_block_state))
314
+
315
+ class PanelDag(Dag):
316
+ def __init__(self, *, site: str='Panel Dag', title: str, doc: str):
317
+ super().__init__(site=site, title=title, doc=doc)
318
+
319
+ def show(self):
320
+ _show_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,72 @@
1
+ Metadata-Version: 2.1
2
+ Name: sier2
3
+ Version: 0.17
4
+ Summary: Block code dags
5
+ Author: algol60
6
+ Author-email: algol60@users.noreply.github.com
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Dist: holoviews (>=1.18.3,<2.0.0)
18
+ Requires-Dist: panel (>=1.4.4)
19
+ Requires-Dist: param (>=2.1.0)
20
+ Description-Content-Type: text/x-rst
21
+
22
+ Sier2
23
+ ======
24
+
25
+ Connect modular pieces of Python code ("blocks") into
26
+ a processing dag pipeline. Blocks are an improvement on libraries;
27
+ if you have a library, you still need to build an application.
28
+ Blocks are pieces of an application, you just have to connect them.
29
+
30
+ See the ``examples`` directory for examples.
31
+
32
+ Description
33
+ -----------
34
+
35
+ A ``block`` is a self-contained piece of code with input and output parameters.
36
+ Blocks can be connected to each other using a ``Dag`` to create
37
+ a dag of blocks.
38
+
39
+ More precisely, output parameters in one block can be connected to input parameters
40
+ in another block. The connections need not be one-to-one: parameters in multiple blocks
41
+ can be connected to parameters in a single block; conversely, parameters in a single block
42
+ can be connected to parameters in multiple blocks.
43
+
44
+ Block parameters use `param <https://param.holoviz.org/>`_, which not only implement
45
+ triggering and watching of events, but allow parameters to be named and documented.
46
+
47
+ A typical block implementation looks like this.
48
+
49
+ .. code-block:: python
50
+
51
+ from sier2 import Block
52
+
53
+ class Increment(Block):
54
+ """A block that adds one to the input value."""
55
+
56
+ int_in = param.Integer(label='The input', doc='An integer')
57
+ int_out = param.Integer(label='The output', doc='The incremented value')
58
+
59
+ def execute(self):
60
+ self.int_out = self.int_in + 1
61
+
62
+ See the examples in ``examples`` (Python scripts) and ``examples-panel`` (scripts that use `Panel <https://panel.holoviz.org/>`_ as a UI).
63
+
64
+ Documentation
65
+ -------------
66
+
67
+ To build the documentation from the repository root directory:
68
+
69
+ .. code-block:: powershell
70
+
71
+ docs/make html
72
+
@@ -0,0 +1,16 @@
1
+ sier2/__init__.py,sha256=evhmBhh3aIfP-yu72pBZbv-3ZwillUHLxqip74zdvBg,154
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
5
+ sier2/_library.py,sha256=aG1f6xHE2qtzvlFCgIp0cMXd6OKlkW_OvbQQb-b2l_8,7536
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=g68zXfyqzcTiBlaC9n_UXTlWtJDdDNCIwLqmJEjgyTQ,10890
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any