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/_library.py ADDED
@@ -0,0 +1,243 @@
1
+ from collections.abc import Iterable
2
+ from dataclasses import dataclass
3
+ import importlib
4
+ from importlib.metadata import entry_points, EntryPoint
5
+ from typing import Any, cast
6
+ import warnings
7
+
8
+ from sier2 import Block, Dag, Connection, BlockError
9
+ from sier2.panel import PanelDag
10
+
11
+ # Store a mapping from a unique key to a Block class.
12
+ # When plugins are initially scanned, the classes are not loaded.
13
+ #
14
+ _block_library: dict[str, type[Block]|None] = {}
15
+ _dag_library: set[str] = set()
16
+
17
+ @dataclass
18
+ class Info:
19
+ key: str
20
+ doc: str
21
+
22
+ def docstring(func) -> str:
23
+ doc = func.__doc__.strip()
24
+
25
+ return doc.split('\n')[0].strip()
26
+
27
+ def _import_item(key):
28
+ """Look up an object by key.
29
+
30
+ The returned object may be a class (if a Block key) or a function (if a dag key).
31
+
32
+ See the Entry points specification at
33
+ https://packaging.python.org/en/latest/specifications/entry-points/#entry-points.
34
+ """
35
+
36
+ modname, qualname_separator, qualname = key.partition(':')
37
+ try:
38
+ obj = importlib.import_module(modname)
39
+ if qualname_separator:
40
+ for attr in qualname.split('.'):
41
+ obj = getattr(obj, attr)
42
+
43
+ return obj
44
+ except ModuleNotFoundError as e:
45
+ msg = str(e)
46
+ if not qualname_separator:
47
+ msg = f'{msg}. Is there a \':\' missing?'
48
+ raise BlockError(msg)
49
+
50
+ def _find_blocks():
51
+ yield from _find('blocks')
52
+
53
+ def _find_dags():
54
+ yield from _find('dags')
55
+
56
+ def run_dag(dag_name):
57
+ """Run the named dag."""
58
+
59
+ ix = dag_name.rfind('.')
60
+ if ix==-1:
61
+ found_dag = None
62
+ for _, d in _find_dags():
63
+ dparts = d.key.split('.')
64
+ if dparts[-1]==dag_name:
65
+ if found_dag:
66
+ raise BlockError(f'Found duplicate: {dag_name}, d')
67
+
68
+ found_dag = d
69
+
70
+ if found_dag is None:
71
+ raise BlockError('No such dag')
72
+
73
+ dag_name = found_dag.key
74
+ ix = dag_name.rfind('.')
75
+
76
+ m = importlib.import_module(dag_name[:ix])
77
+ func = getattr(m, dag_name[ix+1:])
78
+ # if not issubclass(cls, Block):
79
+ # raise BlockError(f'{key} is not a block')
80
+
81
+ dag = func()
82
+ if not hasattr(dag, 'show'):
83
+ raise BlockError(f'Dag {dag_name} does not have a user interface')
84
+
85
+ dag.show()
86
+
87
+ def _find(func_name: str) -> Iterable[tuple[EntryPoint, Info]]:
88
+ """Use ``importlib.metadata.entry_points`` to look up entry points named ``sier2.library``.
89
+
90
+ For each entry point, call ``load()`` to get a module,
91
+ then call ``getattr(module, func_name)()`` to get a list of
92
+ ``BlockInfo`` instances.
93
+
94
+ Parameters
95
+ ----------
96
+ func_name: str
97
+ The name of the function that will be called to get a list[Info].
98
+ Either ``'blocks'`` or ``'dags'``.
99
+ """
100
+
101
+ library = entry_points(group='sier2.library')
102
+
103
+ for entry_point in library:
104
+ try:
105
+ lib = entry_point.load()
106
+ func = getattr(lib, func_name, None)
107
+ if func is not None:
108
+ if not callable(func):
109
+ warnings.warn(f'In {entry_point.module}, {func} is not a function')
110
+ else:
111
+ info_list: list[Info] = func()
112
+ if not isinstance(info_list, list) or any(not isinstance(s, Info) for s in info_list):
113
+ warnings.warn(f'In {entry_point.module}, {func} does not return a list of {Info.__name__} instances')
114
+ else:
115
+ for gi in info_list:
116
+ yield entry_point, gi
117
+ except Exception as e:
118
+ raise BlockError(f'While loading {entry_point}: {e}') from e
119
+
120
+ class Library:
121
+ @staticmethod
122
+ def collect_blocks():
123
+ """Collect block information.
124
+
125
+ Use ``_find_blocks()`` to yield ``BlockInfo`` instances.
126
+
127
+ Note that we don't load the blocks here. We don't want to import
128
+ any modules: this would cause every block module to be imported,
129
+ which would cause a lot of imports to happen. Therefore, we just
130
+ create the keys in the dictionary, and let ``get_block()`` import
131
+ block modules as required.
132
+ """
133
+
134
+ for entry_point, gi in _find_blocks():
135
+ if gi.key in _block_library:
136
+ warnings.warn(f'Block plugin {entry_point}: key {gi.key} already in library')
137
+ else:
138
+ _block_library[gi.key] = None
139
+
140
+ @staticmethod
141
+ def collect_dags():
142
+ for entry_point, gi in _find_dags():
143
+ if gi.key in _dag_library:
144
+ warnings.warn(f'Dag plugin {entry_point}: key {gi.key} already in library')
145
+ else:
146
+ _dag_library.add(gi.key)
147
+
148
+ @staticmethod
149
+ def add_block(block_class: type[Block], key: str|None=None):
150
+ """Add a local block class to the library.
151
+
152
+ The library initially loads block classes using Python's entry_points() mechanism.
153
+ This method allows local Blocks to be added to the library.
154
+
155
+ This is useful for testing, for example.
156
+
157
+ Parameters
158
+ ----------
159
+ block_class: type[Block]
160
+ The Block's class.
161
+ key: str
162
+ The Block's unique key string. By default, the block's block_key()
163
+ class method will be used to obtain the key.
164
+ """
165
+
166
+ if not issubclass(block_class, Block):
167
+ print(f'{key} is not a Block')
168
+
169
+ key_ = key if key else block_class.block_key()
170
+
171
+ if key_ in _block_library:
172
+ raise BlockError(f'Block {key_} is already in the library')
173
+
174
+ _block_library[key_] = block_class
175
+
176
+ @staticmethod
177
+ def get_block(key: str) -> type[Block]:
178
+ if not _block_library:
179
+ Library.collect_blocks()
180
+
181
+ if key not in _block_library:
182
+ raise BlockError(f'Block name {key} is not in the library')
183
+
184
+ if _block_library[key] is None:
185
+ cls = _import_item(key)
186
+ if not issubclass(cls, Block):
187
+ raise BlockError(f'{key} is not a block')
188
+
189
+ # The fully qualified name of the class is probably not the same as
190
+ # the library key string. This matters when the dag is dumped and loaded.
191
+ # Therefore we tell the class what its key is so the key can be dumped,
192
+ # and when the dag is loaded, the block can be found using
193
+ # Library.get_block().
194
+ #
195
+ setattr(cls, Block.SIER2_KEY, key)
196
+
197
+ _block_library[key] = cls
198
+
199
+ return cast(type[Block], _block_library[key])
200
+
201
+ @staticmethod
202
+ def get_dag(key: str) -> type[Dag]:
203
+ if not _dag_library:
204
+ Library.collect_dags()
205
+
206
+ if key not in _dag_library:
207
+ raise BlockError(f'Dag name {key} is not in the library')
208
+
209
+ if key in _dag_library:
210
+ func = _import_item(key)
211
+ dag = func()
212
+ if not isinstance(dag, Dag):
213
+ raise BlockError(f'{key} is not a dag')
214
+
215
+ return cast(type[Dag], dag)
216
+
217
+ @staticmethod
218
+ def load_dag(dump: dict[str, Any]) -> Dag:
219
+ """Load a dag from a serialised structure produced by Block.dump()."""
220
+
221
+ # Create new instances of the specified blocks.
222
+ #
223
+ instances = {}
224
+ for g in dump['blocks']:
225
+ block_key = g['block']
226
+ instance = g['instance']
227
+ if instance not in instances:
228
+ gclass = Library.get_block(block_key)
229
+ instances[instance] = gclass(**g['args'])
230
+ else:
231
+ raise BlockError(f'Instance {instance} ({block_key}) already exists')
232
+
233
+ # Connect the blocks.
234
+ #
235
+ DagType = PanelDag if dump['dag']['type']=='PanelDag' else Dag
236
+ dag = DagType(doc=dump['dag']['doc'], site=dump['dag']['site'], title=dump['dag']['title'])
237
+ for conn in dump['connections']:
238
+ conns = [Connection(**kwargs) for kwargs in conn['conn_args']]
239
+ dag.connect(instances[conn['src']], instances[conn['dst']], *conns)
240
+
241
+ return dag
242
+
243
+ # Library.collect()
sier2/_logger.py ADDED
@@ -0,0 +1,65 @@
1
+ import logging
2
+
3
+ _BLOCK_FORMATTER = logging.Formatter('%(asctime)s %(levelname)s [%(block_name)s] %(message)s', datefmt='%H:%M:%S')
4
+ # formatter = logging.Formatter('%(asctime)s %(levelname)s [%(block_name)s] - %(levelname)s - %(message)s', datefmt='%H:%M:%S')
5
+
6
+ # class BlockHandler(logging.StreamHandler):
7
+ # def format(self, record):
8
+ # fmt = info_formatter# if record.levelno==logging.INFO else formatter
9
+
10
+ # return fmt.format(record)
11
+
12
+ class BlockAdapter(logging.LoggerAdapter):
13
+ """An adapter that log messages from blocks.
14
+
15
+ Each block has its own adapter, so the log automatically includes the block name.
16
+ """
17
+
18
+ def __init__(self, logger, block_name: str, block_state):
19
+ super().__init__(logger)
20
+ self.block_name = block_name
21
+ self.block_state = block_state
22
+
23
+ def debug(self, msg, *args):
24
+ super().debug(msg, *args, extra={'block_name': self.block_name, 'block_state': self.block_state})
25
+
26
+ def info(self, msg, *args):
27
+ super().info(msg, *args, extra={'block_name': self.block_name, 'block_state': self.block_state})
28
+
29
+ def warning(self, msg, *args):
30
+ super().warning(msg, *args, extra={'block_name': self.block_name, 'block_state': self.block_state})
31
+
32
+ def error(self, msg, *args):
33
+ super().error(msg, *args, extra={'block_name': self.block_name, 'block_state': self.block_state})
34
+
35
+ def exception(self, msg, *args, exc_info=True):
36
+ super().error(msg, *args, exc_info=exc_info, extra={'block_name': self.block_name, 'block_state': self.block_state})
37
+
38
+ def critical(self, msg, *args):
39
+ super().critical(msg, *args, extra={'block_name': self.block_name, 'block_state': self.block_state})
40
+
41
+ def process(self, msg, kwargs):
42
+ # print(f'BLOCKADAPTER {msg=} {kwargs=} {self.extra=}')
43
+ if 'block_state' not in kwargs['extra']:
44
+ kwargs['extra']['block_state'] = '?'
45
+ if 'block_name' not in kwargs['extra']:
46
+ kwargs['extra']['block_name'] = 'g'
47
+
48
+ return msg, kwargs
49
+
50
+ _logger = logging.getLogger('block.stream')
51
+ _logger.setLevel(logging.INFO)
52
+
53
+ # _ph = BlockHandler()
54
+ # _ph.setLevel(logging.DEBUG)
55
+
56
+ _ph = logging.StreamHandler()
57
+ _ph.setFormatter(_BLOCK_FORMATTER)
58
+ _ph.setLevel(logging.DEBUG)
59
+
60
+ _logger.addHandler(_ph)
61
+
62
+ def get_logger(block_name: str):
63
+ adapter = BlockAdapter(_logger, block_name, None)
64
+
65
+ return adapter
sier2/_util.py ADDED
@@ -0,0 +1,65 @@
1
+ import sys
2
+
3
+ ########
4
+ # Documentation utilities
5
+ ########
6
+
7
+ def trim(docstring):
8
+ """From PEP-257: Fix docstring indentation"""
9
+
10
+ if not docstring:
11
+ return ''
12
+ # Convert tabs to spaces (following the normal Python rules)
13
+ # and split into a list of lines:
14
+ lines = docstring.expandtabs().splitlines()
15
+ # Determine minimum indentation (first line doesn't count):
16
+ indent = sys.maxsize
17
+ for line in lines[1:]:
18
+ stripped = line.lstrip()
19
+ if stripped:
20
+ indent = min(indent, len(line) - len(stripped))
21
+ # Remove indentation (first line is special):
22
+ trimmed = [lines[0].strip()]
23
+ if indent < sys.maxsize:
24
+ for line in lines[1:]:
25
+ trimmed.append(line[indent:].rstrip())
26
+ # Strip off trailing and leading blank lines:
27
+ while trimmed and not trimmed[-1]:
28
+ trimmed.pop()
29
+ while trimmed and not trimmed[0]:
30
+ trimmed.pop(0)
31
+ # Return a single string:
32
+ return '\n'.join(trimmed)
33
+
34
+ def block_doc_text(block):
35
+ """Generate text documentation for a block.
36
+
37
+ The documentation is taken from the docstring of the block class
38
+ and the doc of each 'in_' and 'out_' param.
39
+ """
40
+
41
+ # Force the first line of the block docstring to have a level 2 header.
42
+ #
43
+ b_doc = '## ' + trim(block.__doc__).lstrip(' #')
44
+
45
+ params = []
46
+ for name, p in block.param.objects().items():
47
+ if name.startswith(('in_', 'out_')):
48
+ doc = p.doc if p.doc else ''
49
+ params.append((name, doc.strip()))
50
+
51
+ params.sort()
52
+ text = []
53
+ for name, doc in params:
54
+ text.append(f'- {name}: {doc}\n')
55
+
56
+ return '---\n' + b_doc + '\n### Params\n' + '\n'.join(text)
57
+
58
+ def dag_doc_text(dag):
59
+ """Generate text documentation for a dag."""
60
+
61
+ # Force the first line of the dag doc to have a level 1 header.
62
+ #
63
+ dag_text =f'# {dag.site} - {dag.title}\n\n# ' + trim(dag.doc).lstrip(' #')
64
+
65
+ return dag_text
sier2/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version('sier2')
@@ -0,0 +1 @@
1
+ from ._panel import PanelDag
@@ -0,0 +1,153 @@
1
+
2
+ """A logger that logs to a panel.widget.Feed."""
3
+
4
+ from datetime import datetime
5
+ import html
6
+ import logging
7
+ import panel as pn
8
+
9
+ from .._block import BlockState
10
+ from ._panel_util import _get_state_color
11
+
12
+ _INFO_FORMATTER = logging.Formatter('%(asctime)s %(block_state)s %(block_name)s %(message)s', datefmt='%H:%M:%S')
13
+ _FORMATTER = logging.Formatter('%(asctime)s %(block_state)s %(block_name)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S')
14
+
15
+ class PanelHandler(logging.Handler):
16
+ """A handler that emits log strings to a panel template sidebar Feed pane."""
17
+
18
+ def __init__(self, log_feed):
19
+ super().__init__()
20
+ self.log_feed = log_feed
21
+
22
+ def format(self, record):
23
+ # TODO override logging.Formatter.formatException to <pre> the exception string.
24
+
25
+ color = _get_state_color(record.block_state)
26
+
27
+ record.block_name = f'[{html.escape(record.block_name)}]' if record.block_name else ''
28
+ record.block_state = f'<span style="color:{color};">■</span>'
29
+ record.msg = html.escape(record.msg)
30
+ fmt = _INFO_FORMATTER if record.levelno==logging.INFO else _FORMATTER
31
+
32
+ return fmt.format(record)
33
+
34
+ def emit(self, record):
35
+ if record.block_state is None:
36
+ self.log_feed.clear()
37
+ return
38
+
39
+ try:
40
+ msg = self.format(record)
41
+ self.log_feed.append(pn.pane.HTML(msg))
42
+ except RecursionError: # See issue 36272
43
+ raise
44
+ except Exception:
45
+ self.handleError(record)
46
+
47
+ class DagPanelAdapter(logging.LoggerAdapter):
48
+ """An adapter that logs messages from a dag.
49
+
50
+ Each message also specifies a block name and state.
51
+ """
52
+
53
+ def debug(self, msg, *args, block_name, block_state):
54
+ super().debug(msg, *args, extra={'block_name': block_name, 'block_state': block_state})
55
+
56
+ def info(self, msg, *args, block_name, block_state):
57
+ super().info(msg, *args, extra={'block_name': block_name, 'block_state': block_state})
58
+
59
+ def warning(self, msg, *args, block_name, block_state):
60
+ super().warning(msg, *args, extra={'block_name': block_name, 'block_state': block_state})
61
+
62
+ def error(self, msg, *args, block_name, block_state):
63
+ super().error(msg, *args, extra={'block_name': block_name, 'block_state': block_state})
64
+
65
+ def exception(self, msg, *args, block_name, block_state):
66
+ super().exception(msg, *args, extra={'block_name': block_name, 'block_state': block_state})
67
+
68
+ def critical(self, msg, *args, block_name, block_state):
69
+ super().critical(msg, *args, extra={'block_name': block_name, 'block_state': block_state})
70
+
71
+ def process(self, msg, kwargs):
72
+ # print(f'ADAPTER {msg=} {kwargs=} {self.extra=}')
73
+ if 'block_state' not in kwargs['extra']:
74
+ kwargs['extra']['block_state'] = '?'
75
+ if 'block_name' not in kwargs['extra']:
76
+ kwargs['extra']['block_name'] = 'g'
77
+
78
+ return msg, kwargs
79
+
80
+ _logger = logging.getLogger('block.panel')
81
+ _logger.setLevel(logging.INFO)
82
+
83
+ # ph = PanelHandler(log_feed)
84
+ # ph.log_feed = log_feed
85
+ # ph.setLevel(logging.INFO)
86
+
87
+ # _logger.addHandler(ph)
88
+
89
+ def getDagPanelLogger(log_feed):
90
+ # _logger = logging.getLogger('block.panel')
91
+ # _logger.setLevel(logging.INFO)
92
+
93
+ ph = PanelHandler(log_feed)
94
+ ph.log_feed = log_feed
95
+ ph.setLevel(logging.INFO)
96
+
97
+ _logger.addHandler(ph)
98
+
99
+ adapter = DagPanelAdapter(_logger)
100
+
101
+ return adapter
102
+
103
+ ####
104
+
105
+ class BlockPanelAdapter(logging.LoggerAdapter):
106
+ """An adapter that logs messages from a block.
107
+
108
+ A state isn't required, because if a block is logging something,
109
+ it's executing by definition.
110
+
111
+ A name isn't required in the logging methods, because the name is
112
+ implicit.
113
+ """
114
+
115
+ def __init__(self, logger, block_name, extra=None):
116
+ super().__init__(logger, extra)
117
+ self.block_name = block_name
118
+
119
+ def debug(self, msg, *args):
120
+ super().debug(msg, *args, extra={'block_name': self.block_name, 'block_state': BlockState.BLOCK})
121
+
122
+ def info(self, msg, *args):
123
+ super().info(msg, *args, extra={'block_name': self.block_name, 'block_state': BlockState.BLOCK})
124
+
125
+ def warning(self, msg, *args):
126
+ super().warning(msg, *args, extra={'block_name': self.block_name, 'block_state': BlockState.BLOCK})
127
+
128
+ def error(self, msg, *args):
129
+ super().error(msg, *args, extra={'block_name': self.block_name, 'block_state': BlockState.BLOCK})
130
+
131
+ def exception(self, msg, *args):
132
+ super().exception(msg, *args, extra={'block_name': self.block_name, 'block_state': BlockState.BLOCK})
133
+
134
+ def critical(self, msg, *args):
135
+ super().critical(msg, *args, extra={'block_name': self.block_name, 'block_state': BlockState.BLOCK})
136
+
137
+ def process(self, msg, kwargs):
138
+ # print(f'GP ADAPTER {msg=} {kwargs=} {self.extra=}')
139
+ if 'block_state' not in kwargs['extra']:
140
+ kwargs['extra']['block_state'] = BlockState.BLOCK
141
+ if 'block_name' not in kwargs['extra']:
142
+ kwargs['extra']['block_name'] = self.block_name
143
+
144
+ return msg, kwargs
145
+
146
+ def getBlockPanelLogger(block_name: str):
147
+ """A logger for blocks.
148
+
149
+ The dag gets its logger first, so we can reuse _logger."""
150
+
151
+ adapter = BlockPanelAdapter(_logger, block_name)
152
+
153
+ return adapter