sier2 0.29__tar.gz → 0.35__tar.gz
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-0.29 → sier2-0.35}/PKG-INFO +1 -1
- {sier2-0.29 → sier2-0.35}/pyproject.toml +1 -1
- {sier2-0.29 → sier2-0.35}/src/sier2/__init__.py +2 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/__main__.py +16 -1
- {sier2-0.29 → sier2-0.35}/src/sier2/_block.py +91 -53
- sier2-0.35/src/sier2/_config.py +223 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/_dag.py +18 -1
- {sier2-0.29 → sier2-0.35}/src/sier2/_library.py +43 -30
- sier2-0.35/src/sier2/_util.py +127 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/panel/_panel.py +35 -8
- sier2-0.29/src/sier2/_util.py +0 -65
- {sier2-0.29 → sier2-0.35}/LICENSE +0 -0
- {sier2-0.29 → sier2-0.35}/README.rst +0 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/_logger.py +0 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/_version.py +0 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/panel/__init__.py +0 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/panel/_feedlogger.py +0 -0
- {sier2-0.29 → sier2-0.35}/src/sier2/panel/_panel_util.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from importlib.metadata import version
|
|
3
3
|
|
|
4
|
-
from sier2 import Library
|
|
4
|
+
from sier2 import Library, Config
|
|
5
5
|
from ._library import _find_blocks, _find_dags, run_dag
|
|
6
6
|
from ._util import block_doc_text, dag_doc_text
|
|
7
7
|
|
|
@@ -66,6 +66,19 @@ def dags_cmd(args):
|
|
|
66
66
|
seen.add(gi.key)
|
|
67
67
|
|
|
68
68
|
def run_cmd(args):
|
|
69
|
+
if args.update_config:
|
|
70
|
+
args = args.update_config.split(',')
|
|
71
|
+
block_name = args[0]
|
|
72
|
+
update_arg = ','.join(args[1:])
|
|
73
|
+
write_to_file = True
|
|
74
|
+
else:
|
|
75
|
+
block_name = None
|
|
76
|
+
update_arg = None
|
|
77
|
+
write_to_file = False
|
|
78
|
+
|
|
79
|
+
if args.config or args.update_config:
|
|
80
|
+
Config.update(location=args.config, config_block=block_name, update_arg=update_arg, write_to_file=write_to_file)
|
|
81
|
+
|
|
69
82
|
run_dag(args.dag)
|
|
70
83
|
|
|
71
84
|
def main():
|
|
@@ -74,6 +87,8 @@ def main():
|
|
|
74
87
|
|
|
75
88
|
run = subparsers.add_parser('run', help='Run a dag')
|
|
76
89
|
run.add_argument('dag', type=str, help='A dag to run')
|
|
90
|
+
run.add_argument('-C', '--config', default=None, help='Use this config file (default to personal config file)')
|
|
91
|
+
run.add_argument('-U', '--update_config', default=None, help='A block that provides a config that updates the personal config file')
|
|
77
92
|
run.set_defaults(func=run_cmd)
|
|
78
93
|
|
|
79
94
|
blocks = subparsers.add_parser('blocks', help='Show available blocks')
|
|
@@ -4,6 +4,7 @@ import param
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from . import _logger
|
|
7
|
+
from ._config import Config
|
|
7
8
|
|
|
8
9
|
class BlockError(Exception):
|
|
9
10
|
"""Raised if a Block configuration is invalid.
|
|
@@ -50,16 +51,34 @@ class Block(param.Parameterized):
|
|
|
50
51
|
class MyBlock(Block):
|
|
51
52
|
...
|
|
52
53
|
|
|
54
|
+
The ``Block`` class inherits from ``param.Parameterized``, and uses parameters
|
|
55
|
+
as described at https://param.holoviz.org/user_guide/Parameters.html.
|
|
56
|
+
There are three kinds of parameters:
|
|
57
|
+
* Input parameters start with ``in_``. These parameters are set before a block is executed.
|
|
58
|
+
* Output parameters start with ``out_``. The block sets these in its ``execute()`` method.
|
|
59
|
+
* Block parameters start with ``block_``. THese are reserved for use by blocks.
|
|
60
|
+
|
|
53
61
|
A typical block will have at least one input parameter, and an ``execute()``
|
|
54
62
|
method that is called when an input parameter value changes.
|
|
55
63
|
|
|
56
64
|
.. code-block:: python
|
|
57
65
|
|
|
58
66
|
class MyBlock(Block):
|
|
59
|
-
|
|
67
|
+
in_value = param.String(label='Input Value')
|
|
68
|
+
out_upper = param.String(label='Output value)
|
|
60
69
|
|
|
61
70
|
def execute(self):
|
|
62
|
-
|
|
71
|
+
self.out_value = self.in_value.upper()
|
|
72
|
+
print(f'New value is {self.out_value}')
|
|
73
|
+
|
|
74
|
+
The block parameter ``block_pause_execution`` allows a block to act as an "input" block,
|
|
75
|
+
particularly when the block hsa a GUI interface. When set to True and dag execution
|
|
76
|
+
reaches this block, the block's ``prepare()`` method is called, then the dag stops executing.
|
|
77
|
+
This allows the user to interact with a user interface.
|
|
78
|
+
|
|
79
|
+
The dag is then restarted using ``dag.execute_after_input(input_block)`` (typically by
|
|
80
|
+
a "Continue" button in the GUI.) When the dag is continued at this block,
|
|
81
|
+
the block's ``execute()`` method is called, and dag execution continues.
|
|
63
82
|
"""
|
|
64
83
|
|
|
65
84
|
block_pause_execution = param.Boolean(default=False, label='Pause execution', doc=_PAUSE_EXECUTION_DOC)
|
|
@@ -68,13 +87,22 @@ class Block(param.Parameterized):
|
|
|
68
87
|
|
|
69
88
|
SIER2_KEY = '_sier2__key'
|
|
70
89
|
|
|
71
|
-
def __init__(self, *args, block_pause_execution=False, continue_label='Continue', **kwargs):
|
|
90
|
+
def __init__(self, *args, block_pause_execution: bool=False, block_doc: str|None=None, continue_label='Continue', **kwargs):
|
|
91
|
+
"""
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
block_pause_execution: bool
|
|
95
|
+
If True, ``prepare()`` is called and dag execution stops.
|
|
96
|
+
block_doc: str|None
|
|
97
|
+
Markdown documentation that may displayed in the user interface.
|
|
98
|
+
"""
|
|
72
99
|
super().__init__(*args, **kwargs)
|
|
73
100
|
|
|
74
101
|
if not self.__doc__:
|
|
75
102
|
raise BlockError(f'Class {self.__class__} must have a docstring')
|
|
76
103
|
|
|
77
104
|
self.block_pause_execution = block_pause_execution
|
|
105
|
+
self.block_doc = block_doc
|
|
78
106
|
self.continue_label = continue_label
|
|
79
107
|
# self._block_state = BlockState.READY
|
|
80
108
|
self.logger = _logger.get_logger(self.name)
|
|
@@ -111,6 +139,66 @@ class Block(param.Parameterized):
|
|
|
111
139
|
|
|
112
140
|
return f'{im.__name__}.{cls.__qualname__}'
|
|
113
141
|
|
|
142
|
+
def get_config(self, *, block: 'Block'=None):
|
|
143
|
+
"""Return a dictionary containing keys and values from the section specified by
|
|
144
|
+
the block in the sier2 config file.
|
|
145
|
+
|
|
146
|
+
The config file has the format described by the Python ``configparser`` module,
|
|
147
|
+
with the added feature that values are evaluated using :func:`ast.literal_eval`,
|
|
148
|
+
and therefore must be syntactically correct Python literals.
|
|
149
|
+
|
|
150
|
+
They keys and values are read from the section ``[block.name]``, where ``name`` is
|
|
151
|
+
this block's unique key as specified by :func:`sier2.Block.block_key`.
|
|
152
|
+
If the ``block`` parameter is unspecified, the calling block is used by default.
|
|
153
|
+
|
|
154
|
+
If the section is not present in the config file, an empty dictionary is returned.
|
|
155
|
+
|
|
156
|
+
The default config file is looked for at
|
|
157
|
+
(the default user config directory) / 'sier2sier2.ini'.
|
|
158
|
+
On Windows, the config directory is ``$ENV:APPDATA``; on Linux, ``$XDG_CONFIG_HOME``
|
|
159
|
+
or ``$HOME/.config``.
|
|
160
|
+
|
|
161
|
+
An alternative config file can be specified by setting ``Config.location`` before
|
|
162
|
+
any dag or block is executed. THis can be done from a command line using
|
|
163
|
+
the ``--config`` option.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
block: Block
|
|
168
|
+
The specified block's config section will be returned. Defaults to ``self``.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
A dictionary containing the section's keys and values.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
b = block if block is not None else self
|
|
176
|
+
name = f'block.{b.block_key()}'
|
|
177
|
+
|
|
178
|
+
return Config[name]
|
|
179
|
+
|
|
180
|
+
def get_config_value(self, key: str, default: Any=None, *, block: 'Block'=None):
|
|
181
|
+
"""Return an individual value from the section specified by
|
|
182
|
+
the block in the sier2 config file.
|
|
183
|
+
|
|
184
|
+
See :func:`sier2.Block.get_config` for more details.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
key: str
|
|
189
|
+
The key of the value to be returned.
|
|
190
|
+
default: Any
|
|
191
|
+
The default value to return if the section or key are not present in the config file.
|
|
192
|
+
block: Block
|
|
193
|
+
The specified block's config section will be returned. Defaults to ``self``.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
b = block if block is not None else self
|
|
197
|
+
name = f'block.{b.block_key()}'
|
|
198
|
+
value = Config[name, key]
|
|
199
|
+
|
|
200
|
+
return value if value is not None else default
|
|
201
|
+
|
|
114
202
|
def prepare(self):
|
|
115
203
|
"""If blockpause_execution is True, called by a dag before calling ``execute()```.
|
|
116
204
|
|
|
@@ -140,25 +228,6 @@ class Block(param.Parameterized):
|
|
|
140
228
|
# print(f'** EXECUTE {self.__class__=}')
|
|
141
229
|
pass
|
|
142
230
|
|
|
143
|
-
# def __panel__(self):
|
|
144
|
-
# """A default Panel component.
|
|
145
|
-
|
|
146
|
-
# When run in a Panel context, a block will typically implement
|
|
147
|
-
# its own __panel__() method. If it doesn't, this method will be
|
|
148
|
-
# used as a default. When a block without a __panel__() is wrapped
|
|
149
|
-
# in a Card, self.progress will be assigned a pn.indicators.Progress()
|
|
150
|
-
# widget which is returned here. The Panel context will make it active
|
|
151
|
-
# before executing the block, and non-active after executing the block.
|
|
152
|
-
# (Why not have a default Progress()? Because we don't want any
|
|
153
|
-
# Panel-related code in the core implementation.)
|
|
154
|
-
|
|
155
|
-
# If the block implements __panel__(), this will obviously be overridden.
|
|
156
|
-
|
|
157
|
-
# When run in non-Panel context, this will remain unused.
|
|
158
|
-
# """
|
|
159
|
-
|
|
160
|
-
# return self._progress
|
|
161
|
-
|
|
162
231
|
def __call__(self, **kwargs) -> dict[str, Any]:
|
|
163
232
|
"""Allow a block to be called directly."""
|
|
164
233
|
|
|
@@ -177,37 +246,6 @@ class Block(param.Parameterized):
|
|
|
177
246
|
|
|
178
247
|
return result
|
|
179
248
|
|
|
180
|
-
# class InputBlock(Block):
|
|
181
|
-
# """A ``Block`` that accepts user input.
|
|
182
|
-
|
|
183
|
-
# An ``InputBlock`` executes in two steps().
|
|
184
|
-
|
|
185
|
-
# When the block is executed by a dag, the dag first sets the input
|
|
186
|
-
# params, then calls ``prepare()``. Execution of the dag then stops.
|
|
187
|
-
|
|
188
|
-
# The dag is then restarted using ``dag.execute_after_input(input_block)``.
|
|
189
|
-
# (An input block must be specified because it is not required that the
|
|
190
|
-
# same input block be used immediately.) This causes the block's
|
|
191
|
-
# ``execute()`` method to be called without resetting the input params.
|
|
192
|
-
|
|
193
|
-
# Dag execution then continues as normal.
|
|
194
|
-
# """
|
|
195
|
-
|
|
196
|
-
# def __init__(self, *args, continue_label='Continue', **kwargs):
|
|
197
|
-
# super().__init__(*args, continue_label=continue_label, **kwargs)
|
|
198
|
-
# self._block_state = BlockState.INPUT
|
|
199
|
-
|
|
200
|
-
# def prepare(self):
|
|
201
|
-
# """Called by a dag before calling ``execute()```.
|
|
202
|
-
|
|
203
|
-
# This gives the block author an opportunity to validate the
|
|
204
|
-
# input params and set up a user inteface.
|
|
205
|
-
|
|
206
|
-
# After the dag restarts on this block, ``execute()`` will be called.
|
|
207
|
-
# """
|
|
208
|
-
|
|
209
|
-
# pass
|
|
210
|
-
|
|
211
249
|
class BlockValidateError(BlockError):
|
|
212
250
|
"""Raised if ``Block.prepare()`` or ``Block.execute()`` determines that input data is invalid.
|
|
213
251
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# A configuration module.
|
|
2
|
+
#
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import configparser
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
# If this key exists in a section and has the value False,
|
|
11
|
+
# this section will not be updated.
|
|
12
|
+
#
|
|
13
|
+
CONFIG_UPDATE = 'config_update'
|
|
14
|
+
|
|
15
|
+
def _default_config_file():
|
|
16
|
+
if os.name=='nt':
|
|
17
|
+
prdir = Path(os.environ['APPDATA']) / 'sier2'
|
|
18
|
+
else:
|
|
19
|
+
prdir = os.environ.get('XDG_CONFIG_HOME', None)
|
|
20
|
+
if prdir:
|
|
21
|
+
prdir = Path(prdir)
|
|
22
|
+
else:
|
|
23
|
+
prdir = Path.home() / '.config'
|
|
24
|
+
|
|
25
|
+
prdir = prdir / 'sier2'
|
|
26
|
+
|
|
27
|
+
if not prdir.exists():
|
|
28
|
+
prdir.mkdir(parents=True)
|
|
29
|
+
|
|
30
|
+
return prdir / 'sier2.ini'
|
|
31
|
+
|
|
32
|
+
class _Config:
|
|
33
|
+
"""This class is for internal use.
|
|
34
|
+
|
|
35
|
+
A single instance of this class is exposed publicly as ``Config``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self._clear()
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def update(*, location: Path|str|None=None, config_block: str|None=None, update_arg: str|None=None, write_to_file: bool=False):
|
|
43
|
+
"""Update the config.
|
|
44
|
+
|
|
45
|
+
If ``location`` has a value, that config file will be used instead of the default.
|
|
46
|
+
|
|
47
|
+
If config_block has a value, it must be the name of a block that has
|
|
48
|
+
an ``out_config`` param. The ``out_config`` param must contain a string that is
|
|
49
|
+
the content of a sier2 ini file. If ``update_arg`` is specified and the block has
|
|
50
|
+
an ``in_arg`` param, ``in_arg`` is set to ``update_arg``.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
location: Path|str|None
|
|
55
|
+
The location of a config file that will be loaded when a config is read.
|
|
56
|
+
config_block: str|None
|
|
57
|
+
A sier2 block that returns the contents of a config file in ``out_config``.
|
|
58
|
+
update_arg: str|None
|
|
59
|
+
A string that is passed to the ``config_block`` block is it has an ``in_arg`` param.
|
|
60
|
+
write_to_file: bool
|
|
61
|
+
If True, the config file at ``location`` is overwritten with the merged config.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if location:
|
|
65
|
+
Config.location = location
|
|
66
|
+
|
|
67
|
+
if config_block:
|
|
68
|
+
# Import here, otherwise there's a circular dependency Library -> Config -> Library.
|
|
69
|
+
# Config blocks better not have any config.
|
|
70
|
+
#
|
|
71
|
+
from sier2 import Library
|
|
72
|
+
|
|
73
|
+
block = Library.get_block(config_block)()
|
|
74
|
+
|
|
75
|
+
if not hasattr(block, 'out_config'):
|
|
76
|
+
raise ValueError('config block does not have out param "out_config"')
|
|
77
|
+
|
|
78
|
+
if hasattr(block, 'in_arg'):
|
|
79
|
+
block.in_arg = update_arg
|
|
80
|
+
|
|
81
|
+
block.execute()
|
|
82
|
+
Config._update(block.out_config, write_to_file=write_to_file)
|
|
83
|
+
|
|
84
|
+
def _clear(self):
|
|
85
|
+
self._location = _default_config_file()
|
|
86
|
+
self._config = {}
|
|
87
|
+
self._loaded = False
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def location(self) -> Path:
|
|
91
|
+
"""Get or set the config file location.
|
|
92
|
+
|
|
93
|
+
By default, this is `$ENV:APPDATA/sier2/sier2.ini` on Window,
|
|
94
|
+
and `$XDG_CONFIG_HOME/sier2/sier2.ini` on Linux.
|
|
95
|
+
|
|
96
|
+
The location cannot be set if the config file has already been loaded.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
return self._location
|
|
100
|
+
|
|
101
|
+
@location.setter
|
|
102
|
+
def location(self, config_file: Path|str):
|
|
103
|
+
if self._loaded:
|
|
104
|
+
raise ValueError('Config is already loaded')
|
|
105
|
+
|
|
106
|
+
if not isinstance(config_file, Path):
|
|
107
|
+
config_file = Path(config_file)
|
|
108
|
+
|
|
109
|
+
self._location = config_file
|
|
110
|
+
|
|
111
|
+
def _update(self, ini: str, write_to_file: bool=False):
|
|
112
|
+
"""Update the config file using ini, which contains the contents of another config file.
|
|
113
|
+
|
|
114
|
+
The current config is also updated.
|
|
115
|
+
|
|
116
|
+
The config file cannot be updated if it has already been loaded.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
if self._loaded:
|
|
120
|
+
raise ValueError('Config is already loaded')
|
|
121
|
+
|
|
122
|
+
new_config = configparser.ConfigParser()
|
|
123
|
+
new_config.read_string(ini)
|
|
124
|
+
|
|
125
|
+
config = configparser.ConfigParser()
|
|
126
|
+
if self._location.is_file():
|
|
127
|
+
config.read(self._location)
|
|
128
|
+
|
|
129
|
+
for section_name in new_config.sections():
|
|
130
|
+
if not config.has_section(section_name):
|
|
131
|
+
config.add_section(section_name)
|
|
132
|
+
config[section_name].update(new_config[section_name])
|
|
133
|
+
else:
|
|
134
|
+
update_section = True
|
|
135
|
+
if CONFIG_UPDATE in config[section_name]:
|
|
136
|
+
update_section = ast.literal_eval(config[section_name][CONFIG_UPDATE])
|
|
137
|
+
if not isinstance(update_section, bool):
|
|
138
|
+
raise ValueError(f'Value of [{section_name}].{CONFIG_UPDATE} is not a bool')
|
|
139
|
+
|
|
140
|
+
if update_section:
|
|
141
|
+
config[section_name].update(new_config[section_name])
|
|
142
|
+
# for k, v in new_config[section_name].items():
|
|
143
|
+
# config[section_name][k] = new_config[section_name][k]
|
|
144
|
+
|
|
145
|
+
if write_to_file:
|
|
146
|
+
with open(self._location, 'w', encoding='utf-8') as f:
|
|
147
|
+
config.write(f)
|
|
148
|
+
|
|
149
|
+
self._config = config
|
|
150
|
+
self._loaded = True
|
|
151
|
+
|
|
152
|
+
def _load(self):
|
|
153
|
+
"""Load the config.
|
|
154
|
+
|
|
155
|
+
Overwrites any previous config. If the location does not exist, the config will be empty.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
self._config = configparser.ConfigParser()
|
|
159
|
+
if self._location.is_file():
|
|
160
|
+
self._config.read(self._location)
|
|
161
|
+
|
|
162
|
+
# The config has been loaded, even if the file didn't exist.
|
|
163
|
+
#
|
|
164
|
+
self._loaded = True
|
|
165
|
+
|
|
166
|
+
def _load_string(self, sconfig):
|
|
167
|
+
"""For testing."""
|
|
168
|
+
|
|
169
|
+
self._config = configparser.ConfigParser()
|
|
170
|
+
self._config.read_string(sconfig)
|
|
171
|
+
self._loaded = True
|
|
172
|
+
|
|
173
|
+
def __getitem__(self, section_name: str|tuple[str, str]) -> Any|dict[str, Any]:
|
|
174
|
+
"""If section_name is a string, get the config values for the given section name.
|
|
175
|
+
|
|
176
|
+
The config file is lazily loaded. Non-existence of the file is normal.
|
|
177
|
+
The keys and values for the specified section are loaded into a new dictionary,
|
|
178
|
+
which is returned.
|
|
179
|
+
|
|
180
|
+
Since configparser always returns values as strings, the values are evaluated
|
|
181
|
+
using :func:`ast.literal_eval` to be correctly typed. This means that strings in the
|
|
182
|
+
.ini file must be surrounded by quotes.
|
|
183
|
+
|
|
184
|
+
A section name for a block is of the form 'block.block_key_name'.
|
|
185
|
+
|
|
186
|
+
The name need not to exist; if it doesn't, an empty dictionary is returned.
|
|
187
|
+
|
|
188
|
+
If section_name is a tuple, it is interpreted as (section_name, key). If the
|
|
189
|
+
section_name and key exist, the value of the key is returned, else None is returned.
|
|
190
|
+
Only that single value is evaluated.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
if not self._loaded:
|
|
194
|
+
self._load()
|
|
195
|
+
|
|
196
|
+
if isinstance(section_name, tuple):
|
|
197
|
+
section_name, key = section_name
|
|
198
|
+
if section_name not in self._config:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
section = self._config[section_name]
|
|
202
|
+
if key not in section:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
v = section[key]
|
|
207
|
+
return ast.literal_eval(v)
|
|
208
|
+
except ValueError:
|
|
209
|
+
raise ValueError(f'Cannot eval section [{section_name}], key {key}, value {v}')
|
|
210
|
+
|
|
211
|
+
if section_name not in self._config:
|
|
212
|
+
return {}
|
|
213
|
+
|
|
214
|
+
c = {}
|
|
215
|
+
for key, v in self._config[section_name].items():
|
|
216
|
+
try:
|
|
217
|
+
c[key] = ast.literal_eval(v)
|
|
218
|
+
except ValueError:
|
|
219
|
+
raise ValueError(f'Cannot eval section [{section_name}], key {key}, value {v}')
|
|
220
|
+
|
|
221
|
+
return c
|
|
222
|
+
|
|
223
|
+
Config = _Config()
|
|
@@ -144,12 +144,29 @@ _RESTART = ':restart:'
|
|
|
144
144
|
class Dag:
|
|
145
145
|
"""A directed acyclic graph of blocks."""
|
|
146
146
|
|
|
147
|
-
def __init__(self, *, site: str='Block', title: str, doc: str, author: dict[str, str]=None):
|
|
147
|
+
def __init__(self, *, site: str='Block', title: str, doc: str, author: dict[str, str]=None, show_doc: bool=True):
|
|
148
|
+
"""A new dag.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
site: str
|
|
153
|
+
Name of the site.
|
|
154
|
+
title: str
|
|
155
|
+
A title to show in the header.
|
|
156
|
+
doc: str
|
|
157
|
+
Dag documentation.
|
|
158
|
+
author: str
|
|
159
|
+
The dag author.
|
|
160
|
+
show_doc: bool
|
|
161
|
+
Show the dag docstring if True.
|
|
162
|
+
"""
|
|
163
|
+
|
|
148
164
|
self._block_pairs: list[tuple[Block, Block]] = []
|
|
149
165
|
|
|
150
166
|
self.site = site
|
|
151
167
|
self.title = title
|
|
152
168
|
self.doc = doc
|
|
169
|
+
self.show_doc = show_doc
|
|
153
170
|
|
|
154
171
|
if author is not None:
|
|
155
172
|
if 'name' in author and 'email' in author:
|
|
@@ -7,6 +7,7 @@ import warnings
|
|
|
7
7
|
|
|
8
8
|
from sier2 import Block, Dag, Connection, BlockError
|
|
9
9
|
from sier2.panel import PanelDag
|
|
10
|
+
from ._util import _import_item
|
|
10
11
|
|
|
11
12
|
# Store a mapping from a unique key to a Block class.
|
|
12
13
|
# When plugins are initially scanned, the classes are not loaded.
|
|
@@ -24,29 +25,6 @@ def docstring(func) -> str:
|
|
|
24
25
|
|
|
25
26
|
return doc.split('\n')[0].strip()
|
|
26
27
|
|
|
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
28
|
def _find_blocks():
|
|
51
29
|
yield from _find('blocks')
|
|
52
30
|
|
|
@@ -56,11 +34,15 @@ def _find_dags():
|
|
|
56
34
|
def run_dag(dag_name):
|
|
57
35
|
"""Run the named dag."""
|
|
58
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
|
+
|
|
59
41
|
ix = dag_name.rfind('.')
|
|
60
42
|
if ix==-1:
|
|
61
43
|
found_dag = None
|
|
62
44
|
for _, d in _find_dags():
|
|
63
|
-
dparts = d.key.split('.')
|
|
45
|
+
dparts = d.key.replace(':', '.').split('.')
|
|
64
46
|
if dparts[-1]==dag_name:
|
|
65
47
|
if found_dag:
|
|
66
48
|
raise BlockError(f'Found duplicate: {dag_name}, d')
|
|
@@ -73,10 +55,12 @@ def run_dag(dag_name):
|
|
|
73
55
|
dag_name = found_dag.key
|
|
74
56
|
ix = dag_name.rfind('.')
|
|
75
57
|
|
|
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')
|
|
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)
|
|
80
64
|
|
|
81
65
|
dag = func()
|
|
82
66
|
if not hasattr(dag, 'show'):
|
|
@@ -159,7 +143,7 @@ class Library:
|
|
|
159
143
|
block_class: type[Block]
|
|
160
144
|
The Block's class.
|
|
161
145
|
key: str
|
|
162
|
-
The Block's unique key string. By default, the block's block_key
|
|
146
|
+
The Block's unique key string. By default, the block's :func:`sier2.Block.block_key`
|
|
163
147
|
class method will be used to obtain the key.
|
|
164
148
|
"""
|
|
165
149
|
|
|
@@ -175,6 +159,22 @@ class Library:
|
|
|
175
159
|
|
|
176
160
|
@staticmethod
|
|
177
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
178
|
if not _block_library:
|
|
179
179
|
Library.collect_blocks()
|
|
180
180
|
|
|
@@ -199,7 +199,20 @@ class Library:
|
|
|
199
199
|
return cast(type[Block], _block_library[key])
|
|
200
200
|
|
|
201
201
|
@staticmethod
|
|
202
|
-
def get_dag(key: str) ->
|
|
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
|
+
|
|
203
216
|
if not _dag_library:
|
|
204
217
|
Library.collect_dags()
|
|
205
218
|
|
|
@@ -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
|
|
@@ -8,6 +8,7 @@ from typing import Callable
|
|
|
8
8
|
|
|
9
9
|
from sier2 import Block, BlockValidateError, BlockState, Dag, BlockError
|
|
10
10
|
from .._dag import _InputValues
|
|
11
|
+
from .._util import trim
|
|
11
12
|
from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
|
|
12
13
|
from ._panel_util import _get_state_color, dag_doc
|
|
13
14
|
|
|
@@ -54,7 +55,7 @@ class _PanelContext:
|
|
|
54
55
|
"""A context manager to wrap the execution of a block within a dag.
|
|
55
56
|
|
|
56
57
|
This default context manager handles the block state, the stopper,
|
|
57
|
-
and converts block execution errors to
|
|
58
|
+
and converts block execution errors to BlockError exceptions.
|
|
58
59
|
|
|
59
60
|
It also uses the panel UI to provide extra information to the user.
|
|
60
61
|
"""
|
|
@@ -225,11 +226,30 @@ def _prepare_to_show(dag: Dag):
|
|
|
225
226
|
)
|
|
226
227
|
dag_logger = getDagPanelLogger(log_feed)
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
cards = []
|
|
230
|
+
if dag.show_doc:
|
|
231
|
+
# The first line of the dag doc is the card header.
|
|
232
|
+
#
|
|
233
|
+
doc = dag.doc.strip()
|
|
234
|
+
ix = doc.find('\n')
|
|
235
|
+
if ix>=0:
|
|
236
|
+
header = doc[:ix]
|
|
237
|
+
doc = doc[ix:].strip()
|
|
238
|
+
else:
|
|
239
|
+
header = ''
|
|
240
|
+
|
|
241
|
+
name_text = pn.widgets.StaticText(
|
|
242
|
+
value=header,
|
|
243
|
+
css_classes=['card-title'],
|
|
244
|
+
styles={'font-size':'1.17em', 'font-weight':'bold'}
|
|
231
245
|
)
|
|
232
|
-
|
|
246
|
+
|
|
247
|
+
card = pn.Card(pn.pane.Markdown(doc, sizing_mode='stretch_width'), header=pn.Row(name_text), sizing_mode='stretch_width')
|
|
248
|
+
cards.append(card)
|
|
249
|
+
|
|
250
|
+
cards.extend(BlockCard(parent_template=template, dag=dag, w=gw, dag_logger=dag_logger) for gw in dag.get_sorted())
|
|
251
|
+
|
|
252
|
+
template.main.append(pn.Column(*cards))
|
|
233
253
|
template.sidebar.append(
|
|
234
254
|
pn.Column(
|
|
235
255
|
switch,
|
|
@@ -315,6 +335,10 @@ class BlockCard(pn.Card):
|
|
|
315
335
|
)
|
|
316
336
|
)
|
|
317
337
|
|
|
338
|
+
# Does this block have documentation to be displayed in the card?
|
|
339
|
+
#
|
|
340
|
+
doc = pn.pane.Markdown(w.block_doc, sizing_mode='scale_width') if w.block_doc else None
|
|
341
|
+
|
|
318
342
|
# If a block has no __panel__() method, Panel will by default
|
|
319
343
|
# inspect the class and display the param attributes.
|
|
320
344
|
# This is obviously not what we want.
|
|
@@ -369,14 +393,17 @@ class BlockCard(pn.Card):
|
|
|
369
393
|
pn.state.notifications.error(notif, duration=0)
|
|
370
394
|
finally:
|
|
371
395
|
parent_template.main[0].loading = False
|
|
372
|
-
c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
|
|
396
|
+
c_button = pn.widgets.Button(name=w.continue_label, button_type='primary', align='end')
|
|
373
397
|
c_button.on_click(on_continue)
|
|
374
398
|
|
|
399
|
+
row = [doc, c_button] if doc else [c_button]
|
|
375
400
|
w_ = pn.Column(
|
|
376
401
|
w,
|
|
377
|
-
pn.Row(
|
|
378
|
-
sizing_mode='
|
|
402
|
+
pn.Row(*row),
|
|
403
|
+
sizing_mode='stretch_width'
|
|
379
404
|
)
|
|
405
|
+
elif doc:
|
|
406
|
+
w_ = pn.Column(w, doc)
|
|
380
407
|
else:
|
|
381
408
|
w_ = w
|
|
382
409
|
|
sier2-0.29/src/sier2/_util.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|