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/_library.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
from ._util import _import_item
|
|
11
|
+
|
|
12
|
+
# Store a mapping from a unique key to a Block class.
|
|
13
|
+
# When plugins are initially scanned, the classes are not loaded.
|
|
14
|
+
#
|
|
15
|
+
_block_library: dict[str, type[Block]|None] = {}
|
|
16
|
+
_dag_library: set[str] = set()
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Info:
|
|
20
|
+
key: str
|
|
21
|
+
doc: str
|
|
22
|
+
|
|
23
|
+
def docstring(func) -> str:
|
|
24
|
+
doc = func.__doc__.strip()
|
|
25
|
+
|
|
26
|
+
return doc.split('\n')[0].strip()
|
|
27
|
+
|
|
28
|
+
def _find_blocks():
|
|
29
|
+
yield from _find('blocks')
|
|
30
|
+
|
|
31
|
+
def _find_dags():
|
|
32
|
+
yield from _find('dags')
|
|
33
|
+
|
|
34
|
+
def run_dag(dag_name):
|
|
35
|
+
"""Run the named dag."""
|
|
36
|
+
|
|
37
|
+
# TODO refactor this to allow specifying just the simple name when the re are no dups,
|
|
38
|
+
# but more of the qualified name where there are dups.
|
|
39
|
+
#
|
|
40
|
+
|
|
41
|
+
ix = dag_name.rfind('.')
|
|
42
|
+
if ix==-1:
|
|
43
|
+
found_dag = None
|
|
44
|
+
for _, d in _find_dags():
|
|
45
|
+
dparts = d.key.replace(':', '.').split('.')
|
|
46
|
+
if dparts[-1]==dag_name:
|
|
47
|
+
if found_dag:
|
|
48
|
+
raise BlockError(f'Found duplicate: {dag_name}, d')
|
|
49
|
+
|
|
50
|
+
found_dag = d
|
|
51
|
+
|
|
52
|
+
if found_dag is None:
|
|
53
|
+
raise BlockError('No such dag')
|
|
54
|
+
|
|
55
|
+
dag_name = found_dag.key
|
|
56
|
+
ix = dag_name.rfind('.')
|
|
57
|
+
|
|
58
|
+
# m = importlib.import_module(dag_name[:ix])
|
|
59
|
+
# func = getattr(m, dag_name[ix+1:])
|
|
60
|
+
# # if not issubclass(cls, Block):
|
|
61
|
+
# # raise BlockError(f'{key} is not a block')
|
|
62
|
+
|
|
63
|
+
func = _import_item(found_dag.key)
|
|
64
|
+
|
|
65
|
+
dag = func()
|
|
66
|
+
if not hasattr(dag, 'show'):
|
|
67
|
+
raise BlockError(f'Dag {dag_name} does not have a user interface')
|
|
68
|
+
|
|
69
|
+
dag.show()
|
|
70
|
+
|
|
71
|
+
def _find(func_name: str) -> Iterable[tuple[EntryPoint, Info]]:
|
|
72
|
+
"""Use ``importlib.metadata.entry_points`` to look up entry points named ``sier2.library``.
|
|
73
|
+
|
|
74
|
+
For each entry point, call ``load()`` to get a module,
|
|
75
|
+
then call ``getattr(module, func_name)()`` to get a list of
|
|
76
|
+
``BlockInfo`` instances.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
func_name: str
|
|
81
|
+
The name of the function that will be called to get a list[Info].
|
|
82
|
+
Either ``'blocks'`` or ``'dags'``.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
library = entry_points(group='sier2.library')
|
|
86
|
+
|
|
87
|
+
for entry_point in library:
|
|
88
|
+
try:
|
|
89
|
+
lib = entry_point.load()
|
|
90
|
+
func = getattr(lib, func_name, None)
|
|
91
|
+
if func is not None:
|
|
92
|
+
if not callable(func):
|
|
93
|
+
warnings.warn(f'In {entry_point.module}, {func} is not a function')
|
|
94
|
+
else:
|
|
95
|
+
info_list: list[Info] = func()
|
|
96
|
+
if not isinstance(info_list, list) or any(not isinstance(s, Info) for s in info_list):
|
|
97
|
+
warnings.warn(f'In {entry_point.module}, {func} does not return a list of {Info.__name__} instances')
|
|
98
|
+
else:
|
|
99
|
+
for gi in info_list:
|
|
100
|
+
yield entry_point, gi
|
|
101
|
+
except Exception as e:
|
|
102
|
+
raise BlockError(f'While loading {entry_point}: {e}') from e
|
|
103
|
+
|
|
104
|
+
class Library:
|
|
105
|
+
@staticmethod
|
|
106
|
+
def collect_blocks():
|
|
107
|
+
"""Collect block information.
|
|
108
|
+
|
|
109
|
+
Use ``_find_blocks()`` to yield ``BlockInfo`` instances.
|
|
110
|
+
|
|
111
|
+
Note that we don't load the blocks here. We don't want to import
|
|
112
|
+
any modules: this would cause every block module to be imported,
|
|
113
|
+
which would cause a lot of imports to happen. Therefore, we just
|
|
114
|
+
create the keys in the dictionary, and let ``get_block()`` import
|
|
115
|
+
block modules as required.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
for entry_point, gi in _find_blocks():
|
|
119
|
+
if gi.key in _block_library:
|
|
120
|
+
warnings.warn(f'Block plugin {entry_point}: key {gi.key} already in library')
|
|
121
|
+
else:
|
|
122
|
+
_block_library[gi.key] = None
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def collect_dags():
|
|
126
|
+
for entry_point, gi in _find_dags():
|
|
127
|
+
if gi.key in _dag_library:
|
|
128
|
+
warnings.warn(f'Dag plugin {entry_point}: key {gi.key} already in library')
|
|
129
|
+
else:
|
|
130
|
+
_dag_library.add(gi.key)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def add_block(block_class: type[Block], key: str|None=None):
|
|
134
|
+
"""Add a local block class to the library.
|
|
135
|
+
|
|
136
|
+
The library initially loads block classes using Python's entry_points() mechanism.
|
|
137
|
+
This method allows local Blocks to be added to the library.
|
|
138
|
+
|
|
139
|
+
This is useful for testing, for example.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
block_class: type[Block]
|
|
144
|
+
The Block's class.
|
|
145
|
+
key: str
|
|
146
|
+
The Block's unique key string. By default, the block's :func:`sier2.Block.block_key`
|
|
147
|
+
class method will be used to obtain the key.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
if not issubclass(block_class, Block):
|
|
151
|
+
print(f'{key} is not a Block')
|
|
152
|
+
|
|
153
|
+
key_ = key if key else block_class.block_key()
|
|
154
|
+
|
|
155
|
+
if key_ in _block_library:
|
|
156
|
+
raise BlockError(f'Block {key_} is already in the library')
|
|
157
|
+
|
|
158
|
+
_block_library[key_] = block_class
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def get_block(key: str) -> type[Block]:
|
|
162
|
+
"""Return the block class specified by the key.
|
|
163
|
+
|
|
164
|
+
Note that this is a ``Block`` sub-class, not an instance of a block.
|
|
165
|
+
The class can be used to instantiate one or more blocks.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
key: str
|
|
170
|
+
The unique name of the block class to be returned.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
type[Block]
|
|
175
|
+
A block class.
|
|
176
|
+
"""
|
|
177
|
+
|
|
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) -> Dag:
|
|
203
|
+
"""Return the dag specified by the key.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
key: str
|
|
208
|
+
The unique name of the dag to be returned.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
Dag
|
|
213
|
+
A dag.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
if not _dag_library:
|
|
217
|
+
Library.collect_dags()
|
|
218
|
+
|
|
219
|
+
if key not in _dag_library:
|
|
220
|
+
raise BlockError(f'Dag name {key} is not in the library')
|
|
221
|
+
|
|
222
|
+
if key in _dag_library:
|
|
223
|
+
func = _import_item(key)
|
|
224
|
+
dag = func()
|
|
225
|
+
if not isinstance(dag, Dag):
|
|
226
|
+
raise BlockError(f'{key} is not a dag')
|
|
227
|
+
|
|
228
|
+
return cast(type[Dag], dag)
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def load_dag(dump: dict[str, Any]) -> Dag:
|
|
232
|
+
"""Load a dag from a serialised structure produced by Block.dump()."""
|
|
233
|
+
|
|
234
|
+
# Create new instances of the specified blocks.
|
|
235
|
+
#
|
|
236
|
+
instances = {}
|
|
237
|
+
for g in dump['blocks']:
|
|
238
|
+
block_key = g['block']
|
|
239
|
+
instance = g['instance']
|
|
240
|
+
if instance not in instances:
|
|
241
|
+
gclass = Library.get_block(block_key)
|
|
242
|
+
instances[instance] = gclass(**g['args'])
|
|
243
|
+
else:
|
|
244
|
+
raise BlockError(f'Instance {instance} ({block_key}) already exists')
|
|
245
|
+
|
|
246
|
+
# Connect the blocks.
|
|
247
|
+
#
|
|
248
|
+
DagType = PanelDag if dump['dag']['type']=='PanelDag' else Dag
|
|
249
|
+
dag = DagType(doc=dump['dag']['doc'], site=dump['dag']['site'], title=dump['dag']['title'])
|
|
250
|
+
for conn in dump['connections']:
|
|
251
|
+
conns = [Connection(**kwargs) for kwargs in conn['conn_args']]
|
|
252
|
+
dag.connect(instances[conn['src']], instances[conn['dst']], *conns)
|
|
253
|
+
|
|
254
|
+
return dag
|
|
255
|
+
|
|
256
|
+
# 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,127 @@
|
|
|
1
|
+
from functools import cache
|
|
2
|
+
import importlib
|
|
3
|
+
from importlib.metadata import entry_points
|
|
4
|
+
import sys
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
from sier2 import BlockError
|
|
8
|
+
|
|
9
|
+
def _import_item(key):
|
|
10
|
+
"""Look up an object by key.
|
|
11
|
+
|
|
12
|
+
The returned object may be a class (if a Block key) or a function (if a dag key).
|
|
13
|
+
|
|
14
|
+
See the Entry points specification at
|
|
15
|
+
https://packaging.python.org/en/latest/specifications/entry-points/#entry-points.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
modname, qualname_separator, qualname = key.partition(':')
|
|
19
|
+
try:
|
|
20
|
+
obj = importlib.import_module(modname)
|
|
21
|
+
if qualname_separator:
|
|
22
|
+
for attr in qualname.split('.'):
|
|
23
|
+
obj = getattr(obj, attr)
|
|
24
|
+
|
|
25
|
+
return obj
|
|
26
|
+
except ModuleNotFoundError as e:
|
|
27
|
+
msg = str(e)
|
|
28
|
+
if not qualname_separator:
|
|
29
|
+
msg = f'{msg}. Is there a \':\' missing?'
|
|
30
|
+
raise BlockError(msg)
|
|
31
|
+
|
|
32
|
+
@cache
|
|
33
|
+
def get_block_config():
|
|
34
|
+
"""A convenience function to get block configuration data.
|
|
35
|
+
|
|
36
|
+
Block can run in different environments; for example, a block that has access to the
|
|
37
|
+
Internet may use a different configuration to the same block running in a corporate
|
|
38
|
+
environment.
|
|
39
|
+
|
|
40
|
+
This function looks up a block configuration provider using the ``sier2.config`` entry point,
|
|
41
|
+
which has the form `module-name:function-name`.
|
|
42
|
+
|
|
43
|
+
If no config package is found, or more than one config package is found,
|
|
44
|
+
a warning will be produced (using `warnings.warn()`), and a default config will be returned,
|
|
45
|
+
with the ``'config'`` key having the value ``None``.
|
|
46
|
+
|
|
47
|
+
See the `sier2-blocks-config` package for an example.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
eps = list(entry_points(group='sier2.config'))
|
|
51
|
+
if len(eps)==1:
|
|
52
|
+
ep = eps[0].value
|
|
53
|
+
config_func = _import_item(ep)
|
|
54
|
+
config = config_func()
|
|
55
|
+
else:
|
|
56
|
+
msg = 'No block configuration found' if not eps else 'Multiple configs found'
|
|
57
|
+
warnings.warn(f'{msg}: returning config None')
|
|
58
|
+
config = {'config': None}
|
|
59
|
+
|
|
60
|
+
if 'config' not in config:
|
|
61
|
+
raise BlockError('config dictionary does not contain "config" key')
|
|
62
|
+
|
|
63
|
+
return config
|
|
64
|
+
|
|
65
|
+
########
|
|
66
|
+
# Documentation utilities
|
|
67
|
+
########
|
|
68
|
+
|
|
69
|
+
def trim(docstring):
|
|
70
|
+
"""From PEP-257: Fix docstring indentation"""
|
|
71
|
+
|
|
72
|
+
if not docstring:
|
|
73
|
+
return ''
|
|
74
|
+
# Convert tabs to spaces (following the normal Python rules)
|
|
75
|
+
# and split into a list of lines:
|
|
76
|
+
lines = docstring.expandtabs().splitlines()
|
|
77
|
+
# Determine minimum indentation (first line doesn't count):
|
|
78
|
+
indent = sys.maxsize
|
|
79
|
+
for line in lines[1:]:
|
|
80
|
+
stripped = line.lstrip()
|
|
81
|
+
if stripped:
|
|
82
|
+
indent = min(indent, len(line) - len(stripped))
|
|
83
|
+
# Remove indentation (first line is special):
|
|
84
|
+
trimmed = [lines[0].strip()]
|
|
85
|
+
if indent < sys.maxsize:
|
|
86
|
+
for line in lines[1:]:
|
|
87
|
+
trimmed.append(line[indent:].rstrip())
|
|
88
|
+
# Strip off trailing and leading blank lines:
|
|
89
|
+
while trimmed and not trimmed[-1]:
|
|
90
|
+
trimmed.pop()
|
|
91
|
+
while trimmed and not trimmed[0]:
|
|
92
|
+
trimmed.pop(0)
|
|
93
|
+
# Return a single string:
|
|
94
|
+
return '\n'.join(trimmed)
|
|
95
|
+
|
|
96
|
+
def block_doc_text(block):
|
|
97
|
+
"""Generate text documentation for a block.
|
|
98
|
+
|
|
99
|
+
The documentation is taken from the docstring of the block class
|
|
100
|
+
and the doc of each 'in_' and 'out_' param.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Force the first line of the block docstring to have a level 2 header.
|
|
104
|
+
#
|
|
105
|
+
b_doc = '## ' + trim(block.__doc__).lstrip(' #')
|
|
106
|
+
|
|
107
|
+
params = []
|
|
108
|
+
for name, p in block.param.objects().items():
|
|
109
|
+
if name.startswith(('in_', 'out_')):
|
|
110
|
+
doc = p.doc if p.doc else ''
|
|
111
|
+
params.append((name, doc.strip()))
|
|
112
|
+
|
|
113
|
+
params.sort()
|
|
114
|
+
text = []
|
|
115
|
+
for name, doc in params:
|
|
116
|
+
text.append(f'- {name}: {doc}\n')
|
|
117
|
+
|
|
118
|
+
return '---\n' + b_doc + '\n### Params\n' + '\n'.join(text)
|
|
119
|
+
|
|
120
|
+
def dag_doc_text(dag):
|
|
121
|
+
"""Generate text documentation for a dag."""
|
|
122
|
+
|
|
123
|
+
# Force the first line of the dag doc to have a level 1 header.
|
|
124
|
+
#
|
|
125
|
+
dag_text =f'# {dag.site} - {dag.title}\n\n# ' + trim(dag.doc).lstrip(' #')
|
|
126
|
+
|
|
127
|
+
return dag_text
|
sier2/_version.py
ADDED
sier2/panel/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._panel import PanelDag
|