sier2 0.24__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.

@@ -1,23 +1,18 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: sier2
3
- Version: 0.24
3
+ Version: 0.35
4
4
  Summary: Blocks of code that are executed in dags
5
- Author: algol60
6
- Author-email: algol60@users.noreply.github.com
7
- Requires-Python: >=3.11,<4.0
8
- Classifier: Intended Audience :: Developers
9
- Classifier: Intended Audience :: Science/Research
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python :: 3
5
+ Author: Algol60
6
+ Author-email: algol60 <algol60@users.noreply.github.com>
13
7
  Classifier: Programming Language :: Python :: 3.11
14
8
  Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Developers
16
13
  Classifier: Topic :: Scientific/Engineering
17
14
  Classifier: Topic :: Software Development :: Libraries
18
- Requires-Dist: holoviews (>=1.19.0)
19
- Requires-Dist: panel (>=1.4.4)
20
- Requires-Dist: param (>=2.1.0)
15
+ Project-URL: Homepage, https://github.com/algol60/sier2
21
16
  Description-Content-Type: text/x-rst
22
17
 
23
18
  Sier2
@@ -1,8 +1,10 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "sier2"
3
- version = "0.24"
3
+ version = "0.35"
4
4
  description = "Blocks of code that are executed in dags"
5
- authors = ["algol60 <algol60@users.noreply.github.com>"]
5
+ authors = [
6
+ {name="Algol60", email="algol60 <algol60@users.noreply.github.com>"}
7
+ ]
6
8
  readme = "README.rst"
7
9
  packages = [{include = "sier2", from = "src"}]
8
10
  classifiers = [
@@ -16,7 +18,7 @@ classifiers = [
16
18
  "Topic :: Software Development :: Libraries"
17
19
  ]
18
20
 
19
- [tool.poetry.dependencies]
21
+ [dependencies]
20
22
  python = "^3.11"
21
23
 
22
24
  holoviews = ">=1.19.0"
@@ -26,7 +28,8 @@ param = ">=2.1.0"
26
28
  [[tool.mypy.overrides]]
27
29
  module = [
28
30
  "holoviews",
29
- "param"
31
+ "param",
32
+ "networkx",
30
33
  ]
31
34
  ignore_missing_imports = true
32
35
 
@@ -34,5 +37,5 @@ ignore_missing_imports = true
34
37
  Homepage = "https://github.com/algol60/sier2"
35
38
 
36
39
  [build-system]
37
- requires = ["poetry-core"]
40
+ requires = ["poetry-core>=2.1.1"]
38
41
  build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,6 @@
1
+ from ._block import Block, BlockError, BlockValidateError
2
+ from ._config import Config
3
+ from ._dag import Connection, Dag, BlockState
4
+ from ._library import Library, Info
5
+ from ._version import __version__
6
+ from ._util import get_block_config
@@ -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')
@@ -0,0 +1,259 @@
1
+ from enum import StrEnum
2
+ import inspect
3
+ import param
4
+ from typing import Any
5
+
6
+ from . import _logger
7
+ from ._config import Config
8
+
9
+ class BlockError(Exception):
10
+ """Raised if a Block configuration is invalid.
11
+
12
+ If this exception is raised, the executing dag sets its stop
13
+ flag (which must be manually reset), and displays a stacktrace.
14
+ """
15
+
16
+ pass
17
+
18
+ class BlockState(StrEnum):
19
+ """The current state of a block; also used for logging."""
20
+
21
+ DAG = 'DAG' # Dag logging.
22
+ BLOCK = 'BLOCK' # Block logging.
23
+ INPUT = 'INPUT'
24
+ READY = 'READY'
25
+ EXECUTING = 'EXECUTING'
26
+ WAITING = 'WAITING'
27
+ SUCCESSFUL = 'SUCCESSFUL'
28
+ INTERRUPTED = 'INTERRUPTED'
29
+ ERROR = 'ERROR'
30
+
31
+ _PAUSE_EXECUTION_DOC = '''If True, a block executes in two steps.
32
+
33
+ When the block is executed by a dag, the dag first sets the input
34
+ params, then calls ``prepare()``. Execution of the dag then stops.
35
+
36
+ The dag is then restarted using ``dag.execute_after_input(input_block)``.
37
+ (An input block must be specified because it is not required that the
38
+ same input block be used immediately.) This causes the block's
39
+ ``execute()`` method to be called without resetting the input params.
40
+
41
+ Dag execution then continues as normal.
42
+ '''
43
+
44
+ class Block(param.Parameterized):
45
+ """The base class for blocks.
46
+
47
+ A block is implemented as:
48
+
49
+ .. code-block:: python
50
+
51
+ class MyBlock(Block):
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
+
61
+ A typical block will have at least one input parameter, and an ``execute()``
62
+ method that is called when an input parameter value changes.
63
+
64
+ .. code-block:: python
65
+
66
+ class MyBlock(Block):
67
+ in_value = param.String(label='Input Value')
68
+ out_upper = param.String(label='Output value)
69
+
70
+ def execute(self):
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.
82
+ """
83
+
84
+ block_pause_execution = param.Boolean(default=False, label='Pause execution', doc=_PAUSE_EXECUTION_DOC)
85
+
86
+ _block_state = param.String(default=BlockState.READY)
87
+
88
+ SIER2_KEY = '_sier2__key'
89
+
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
+ """
99
+ super().__init__(*args, **kwargs)
100
+
101
+ if not self.__doc__:
102
+ raise BlockError(f'Class {self.__class__} must have a docstring')
103
+
104
+ self.block_pause_execution = block_pause_execution
105
+ self.block_doc = block_doc
106
+ self.continue_label = continue_label
107
+ # self._block_state = BlockState.READY
108
+ self.logger = _logger.get_logger(self.name)
109
+
110
+ # Maintain a map of "block+output parameter being watched" -> "input parameter".
111
+ # This is used by _block_event() to set the correct input parameter.
112
+ #
113
+ self._block_name_map: dict[tuple[str, str], str] = {}
114
+
115
+ # Record this block's output parameters.
116
+ # If this is an input block, we need to trigger
117
+ # the output values before executing the next block,
118
+ # in case the user didn't change anything.
119
+ #
120
+ self._block_out_params = []
121
+
122
+ # self._block_context = _EmptyContext()
123
+
124
+ # self._progress = None
125
+
126
+ @classmethod
127
+ def block_key(cls):
128
+ """The unique key of this block class.
129
+
130
+ Blocks require a unique key so they can be identified in the block library.
131
+ The default implementation should be sufficient, but can be overridden
132
+ in case of refactoring or name clashes.
133
+ """
134
+
135
+ im = inspect.getmodule(cls)
136
+
137
+ if hasattr(cls, Block.SIER2_KEY):
138
+ return getattr(cls, Block.SIER2_KEY)
139
+
140
+ return f'{im.__name__}.{cls.__qualname__}'
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
+
202
+ def prepare(self):
203
+ """If blockpause_execution is True, called by a dag before calling ``execute()```.
204
+
205
+ This gives the block author an opportunity to validate the
206
+ input params and set up a user inteface.
207
+
208
+ After the dag restarts on this block, ``execute()`` will be called.
209
+ """
210
+
211
+ pass
212
+
213
+ def execute(self, *_, **__):
214
+ """This method is called when one or more of the input parameters causes an event.
215
+
216
+ Override this method in a Block subclass.
217
+
218
+ The ``execute()`` method can have arguments. The arguments can be specified
219
+ in any order. It is not necessary to specify all, or any, arguments.
220
+ Arguments will not be passed via ``*args`` or ``**kwargs``.
221
+
222
+ * ``stopper`` - an indicator that the dag has been stopped. This may be
223
+ set while the block is executing, in which case the block should
224
+ stop executing as soon as possible.
225
+ * ``events`` - the param events that caused execute() to be called.
226
+ """
227
+
228
+ # print(f'** EXECUTE {self.__class__=}')
229
+ pass
230
+
231
+ def __call__(self, **kwargs) -> dict[str, Any]:
232
+ """Allow a block to be called directly."""
233
+
234
+ in_names = [name for name in self.__class__.param if name.startswith('in_')]
235
+ if len(kwargs)!=len(in_names) or any(name not in in_names for name in kwargs):
236
+ names = ', '.join(in_names)
237
+ raise BlockError(f'All input params must be specified: {names}')
238
+
239
+ for name, value in kwargs.items():
240
+ setattr(self, name, value)
241
+
242
+ self.execute()
243
+
244
+ out_names = [name for name in self.__class__.param if name.startswith('out_')]
245
+ result = {name: getattr(self, name) for name in out_names}
246
+
247
+ return result
248
+
249
+ class BlockValidateError(BlockError):
250
+ """Raised if ``Block.prepare()`` or ``Block.execute()`` determines that input data is invalid.
251
+
252
+ If this exception is raised, it will be caught by the executing dag.
253
+ The dag will not set its stop flag, no stacktrace will be displayed,
254
+ and the error message will be displayed.
255
+ """
256
+
257
+ def __init__(self, *, block_name: str, error: str):
258
+ super().__init__(error)
259
+ self.block_name = block_name
@@ -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()