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/__init__.py +4 -0
- sier2/__main__.py +96 -0
- sier2/_block.py +148 -0
- sier2/_dag.py +558 -0
- sier2/_library.py +226 -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 +320 -0
- sier2/panel/_panel_util.py +83 -0
- sier2-0.17.dist-info/LICENSE +21 -0
- sier2-0.17.dist-info/METADATA +72 -0
- sier2-0.17.dist-info/RECORD +16 -0
- sier2-0.17.dist-info/WHEEL +4 -0
sier2/_library.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
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 _find_blocks():
|
|
28
|
+
yield from _find('blocks')
|
|
29
|
+
|
|
30
|
+
def _find_dags():
|
|
31
|
+
yield from _find('dags')
|
|
32
|
+
|
|
33
|
+
def run_dag(dag_name):
|
|
34
|
+
"""Run the named dag."""
|
|
35
|
+
|
|
36
|
+
ix = dag_name.rfind('.')
|
|
37
|
+
if ix==-1:
|
|
38
|
+
found_dag = None
|
|
39
|
+
for _, d in _find_dags():
|
|
40
|
+
dparts = d.key.split('.')
|
|
41
|
+
if dparts[-1]==dag_name:
|
|
42
|
+
if found_dag:
|
|
43
|
+
raise BlockError(f'Found duplicate: {dag_name}, d')
|
|
44
|
+
|
|
45
|
+
found_dag = d
|
|
46
|
+
|
|
47
|
+
if found_dag is None:
|
|
48
|
+
raise BlockError('No such dag')
|
|
49
|
+
|
|
50
|
+
dag_name = found_dag.key
|
|
51
|
+
ix = dag_name.rfind('.')
|
|
52
|
+
|
|
53
|
+
m = importlib.import_module(dag_name[:ix])
|
|
54
|
+
func = getattr(m, dag_name[ix+1:])
|
|
55
|
+
# if not issubclass(cls, Block):
|
|
56
|
+
# raise BlockError(f'{key} is not a block')
|
|
57
|
+
|
|
58
|
+
dag = func()
|
|
59
|
+
if not hasattr(dag, 'show'):
|
|
60
|
+
raise BlockError(f'Dag {dag_name} does not have a user interface')
|
|
61
|
+
|
|
62
|
+
dag.show()
|
|
63
|
+
|
|
64
|
+
def _find(func_name: str) -> Iterable[tuple[EntryPoint, Info]]:
|
|
65
|
+
"""Use ``importlib.metadata.entry_points`` to look up entry points named ``sier2.library``.
|
|
66
|
+
|
|
67
|
+
For each entry point, call ``load()`` to get a module,
|
|
68
|
+
then call ``getattr(module, func_name)()`` to get a list of
|
|
69
|
+
``BlockInfo`` instances.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
func_name: str
|
|
74
|
+
The name of the function that will be called to get a list[Info].
|
|
75
|
+
Either ``'blocks'`` or ``'dags'``.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
library = entry_points(group='sier2.library')
|
|
79
|
+
|
|
80
|
+
for entry_point in library:
|
|
81
|
+
try:
|
|
82
|
+
lib = entry_point.load()
|
|
83
|
+
func = getattr(lib, func_name, None)
|
|
84
|
+
if func is not None:
|
|
85
|
+
if not callable(func):
|
|
86
|
+
warnings.warn(f'In {entry_point.module}, {func} is not a function')
|
|
87
|
+
else:
|
|
88
|
+
info_list: list[Info] = func()
|
|
89
|
+
if not isinstance(info_list, list) or any(not isinstance(s, Info) for s in info_list):
|
|
90
|
+
warnings.warn(f'In {entry_point.module}, {func} does not return a list of {Info.__name__} instances')
|
|
91
|
+
else:
|
|
92
|
+
for gi in info_list:
|
|
93
|
+
yield entry_point, gi
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise BlockError(f'While loading {entry_point}: {e}') from e
|
|
96
|
+
|
|
97
|
+
class Library:
|
|
98
|
+
@staticmethod
|
|
99
|
+
def collect_blocks():
|
|
100
|
+
"""Collect block information.
|
|
101
|
+
|
|
102
|
+
Use ``_find_blocks()`` to yield ``BlockInfo`` instances.
|
|
103
|
+
|
|
104
|
+
Note that we don't load the blocks here. We don't want to import
|
|
105
|
+
any modules: this would cause every block module to be imported,
|
|
106
|
+
which would cause a lot of imports to happen. Therefore, we just
|
|
107
|
+
create the keys in the dictionary, and let ``get_block()`` import
|
|
108
|
+
block modules as required.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
for entry_point, gi in _find_blocks():
|
|
112
|
+
if gi.key in _block_library:
|
|
113
|
+
warnings.warn(f'Block plugin {entry_point}: key {gi.key} already in library')
|
|
114
|
+
else:
|
|
115
|
+
_block_library[gi.key] = None
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def collect_dags():
|
|
119
|
+
for entry_point, gi in _find_dags():
|
|
120
|
+
if gi.key in _dag_library:
|
|
121
|
+
warnings.warn(f'Dag plugin {entry_point}: key {gi.key} already in library')
|
|
122
|
+
else:
|
|
123
|
+
_dag_library.add(gi.key)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def add_block(block_class: type[Block], key: str|None=None):
|
|
127
|
+
"""Add a local block class to the library.
|
|
128
|
+
|
|
129
|
+
The library initially loads block classes using Python's entry_points() mechanism.
|
|
130
|
+
This method allows local Blocks to be added to the library.
|
|
131
|
+
|
|
132
|
+
This is useful for testing, for example.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
block_class: type[Block]
|
|
137
|
+
The Block's class.
|
|
138
|
+
key: str
|
|
139
|
+
The Block's unique key string. By default, the block's block_key()
|
|
140
|
+
class method will be used to obtain the key.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
if not issubclass(block_class, Block):
|
|
144
|
+
print(f'{key} is not a Block')
|
|
145
|
+
|
|
146
|
+
# if not key:
|
|
147
|
+
# key = block_class.block_key()
|
|
148
|
+
key_ = key if key else block_class.block_key()
|
|
149
|
+
|
|
150
|
+
if key_ in _block_library:
|
|
151
|
+
raise BlockError(f'Block {key_} is already in the library')
|
|
152
|
+
|
|
153
|
+
_block_library[key_] = block_class
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def get_block(key: str) -> type[Block]:
|
|
157
|
+
if not _block_library:
|
|
158
|
+
Library.collect_blocks()
|
|
159
|
+
|
|
160
|
+
if key not in _block_library:
|
|
161
|
+
raise BlockError(f'Block name {key} is not in the library')
|
|
162
|
+
|
|
163
|
+
if _block_library[key] is None:
|
|
164
|
+
ix = key.rfind('.')
|
|
165
|
+
m = importlib.import_module(key[:ix])
|
|
166
|
+
cls = getattr(m, key[ix+1:])
|
|
167
|
+
if not issubclass(cls, Block):
|
|
168
|
+
raise BlockError(f'{key} is not a block')
|
|
169
|
+
|
|
170
|
+
# The fully qualified name of the class is probably not the same as
|
|
171
|
+
# the library key string. This matters when the dag is dumped and loaded.
|
|
172
|
+
# Therefore we tell the class what its key is so the key can be dumped,
|
|
173
|
+
# and when the dag is loaded, the block can be found using
|
|
174
|
+
# Library.get_block().
|
|
175
|
+
#
|
|
176
|
+
setattr(cls, Block.SIER2_KEY, key)
|
|
177
|
+
|
|
178
|
+
_block_library[key] = cls
|
|
179
|
+
|
|
180
|
+
return cast(type[Block], _block_library[key])
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def get_dag(key: str) -> type[Dag]:
|
|
184
|
+
if not _dag_library:
|
|
185
|
+
Library.collect_dags()
|
|
186
|
+
|
|
187
|
+
if key not in _dag_library:
|
|
188
|
+
raise BlockError(f'Dag name {key} is not in the library')
|
|
189
|
+
|
|
190
|
+
if key in _dag_library:
|
|
191
|
+
ix = key.rfind('.')
|
|
192
|
+
m = importlib.import_module(key[:ix])
|
|
193
|
+
func = getattr(m, key[ix+1:])
|
|
194
|
+
dag = func()
|
|
195
|
+
if not isinstance(dag, Dag):
|
|
196
|
+
raise BlockError(f'{key} is not a dag')
|
|
197
|
+
|
|
198
|
+
return cast(type[Dag], dag)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def load_dag(dump: dict[str, Any]) -> Dag:
|
|
202
|
+
"""Load a dag from a serialised structure produced by Block.dump()."""
|
|
203
|
+
|
|
204
|
+
# Create new instances of the specified blocks.
|
|
205
|
+
#
|
|
206
|
+
instances = {}
|
|
207
|
+
for g in dump['blocks']:
|
|
208
|
+
block_key = g['block']
|
|
209
|
+
instance = g['instance']
|
|
210
|
+
if instance not in instances:
|
|
211
|
+
gclass = Library.get_block(block_key)
|
|
212
|
+
instances[instance] = gclass(**g['args'])
|
|
213
|
+
else:
|
|
214
|
+
raise BlockError(f'Instance {instance} ({block_key}) already exists')
|
|
215
|
+
|
|
216
|
+
# Connect the blocks.
|
|
217
|
+
#
|
|
218
|
+
DagType = PanelDag if dump['dag']['type']=='PanelDag' else Dag
|
|
219
|
+
dag = DagType(doc=dump['dag']['doc'], site=dump['dag']['site'], title=dump['dag']['title'])
|
|
220
|
+
for conn in dump['connections']:
|
|
221
|
+
conns = [Connection(**kwargs) for kwargs in conn['conn_args']]
|
|
222
|
+
dag.connect(instances[conn['src']], instances[conn['dst']], *conns)
|
|
223
|
+
|
|
224
|
+
return dag
|
|
225
|
+
|
|
226
|
+
# 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
sier2/panel/__init__.py
ADDED
|
@@ -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
|