ezmsg 3.3.4__tar.gz → 3.4.0__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.
Files changed (45) hide show
  1. {ezmsg-3.3.4 → ezmsg-3.4.0}/PKG-INFO +8 -11
  2. {ezmsg-3.3.4 → ezmsg-3.4.0}/README.md +7 -10
  3. {ezmsg-3.3.4 → ezmsg-3.4.0}/pyproject.toml +7 -1
  4. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/backend.py +21 -0
  5. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/backendprocess.py +23 -20
  6. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/collection.py +17 -2
  7. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/command.py +2 -1
  8. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/component.py +23 -12
  9. ezmsg-3.4.0/src/ezmsg/core/settings.py +65 -0
  10. ezmsg-3.4.0/src/ezmsg/core/state.py +56 -0
  11. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/stream.py +10 -0
  12. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/unit.py +66 -8
  13. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/debuglog.py +18 -2
  14. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/generator.py +6 -0
  15. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagegate.py +23 -2
  16. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagelogger.py +33 -0
  17. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagequeue.py +14 -0
  18. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagereplay.py +58 -4
  19. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messages/axisarray.py +35 -16
  20. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/terminate.py +30 -2
  21. ezmsg-3.3.4/src/ezmsg/core/settings.py +0 -31
  22. ezmsg-3.3.4/src/ezmsg/core/state.py +0 -29
  23. {ezmsg-3.3.4 → ezmsg-3.4.0}/LICENSE +0 -0
  24. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/__init__.py +0 -0
  25. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/__main__.py +0 -0
  26. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/addressable.py +0 -0
  27. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/backpressure.py +0 -0
  28. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/dag.py +0 -0
  29. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/graphcontext.py +0 -0
  30. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/graphserver.py +0 -0
  31. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/message.py +0 -0
  32. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/messagecache.py +0 -0
  33. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/messagemarshal.py +0 -0
  34. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/netprotocol.py +0 -0
  35. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/pubclient.py +0 -0
  36. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/server.py +0 -0
  37. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/shmserver.py +0 -0
  38. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/subclient.py +0 -0
  39. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/util.py +0 -0
  40. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/__init__.py +0 -0
  41. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/gen_to_unit.py +0 -0
  42. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagecodec.py +0 -0
  43. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messages/__init__.py +0 -0
  44. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messages/modify.py +0 -0
  45. {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/rate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ezmsg
3
- Version: 3.3.4
3
+ Version: 3.4.0
4
4
  Summary: A simple DAG-based computation model
5
5
  License: MIT
6
6
  Author: Milsap, Griffin
@@ -61,7 +61,7 @@ $ source env/bin/activate
61
61
  (env) $ python -m pytest tests # Optionally, Perform tests
62
62
  ```
63
63
 
64
- Note that it is generally recommended to install poetry into it's own standalone venv via the `pipx` cli tool.
64
+ Note that it is generally recommended to install poetry into its own standalone venv via the `pipx` cli tool.
65
65
 
66
66
  ## Documentation
67
67
 
@@ -78,7 +78,7 @@ pip install "ezmsg[all_ext]"
78
78
  ```
79
79
 
80
80
  This will install all the available public extension packages for `ezmsg` that are listed in `pyproject.toml`.
81
- If you prefer to install the extension packages individually, you can use the following command:
81
+ If you prefer to install a subset of extension packages, you can use the following command:
82
82
 
83
83
  ```bash
84
84
  pip install "ezmsg[zmq,sigproc,...]"
@@ -86,17 +86,14 @@ pip install "ezmsg[zmq,sigproc,...]"
86
86
 
87
87
  Please note that the `ezmsg` package itself can still be installed without any additional extensions using `pip install ezmsg`.
88
88
 
89
- See the extension directory for more details
90
-
91
- - `ezmsg-sigproc` -- Timeseries signal processing modules
92
- - `ezmsg-websocket` -- Websocket server and client nodes for `ezmsg` graphs
93
- - `ezmsg-zmq` -- ZeroMQ pub and sub nodes for `ezmsg` graphs
94
- - ... More to come!
95
-
96
- Additionally, the following extensions are contained in external repositories:
89
+ Extensions can be managed manually as well. Here are some of the extensions we manage or are aware of:
97
90
 
91
+ - [ezmsg-sigproc](https://github.com/ezmsg-org/ezmsg-sigproc) -- Timeseries signal processing modules
92
+ - [ezmsg-websocket](https://github.com/ezmsg-org/ezmsg-websocket) -- Websocket server and client nodes for `ezmsg` graphs
93
+ - [ezmsg-zmq](https://github.com/ezmsg-org/ezmsg-zmq) -- ZeroMQ pub and sub nodes for `ezmsg` graphs
98
94
  - [ezmsg-panel](https://github.com/griffinmilsap/ezmsg-panel) -- Plotting tools for `ezmsg` that use [panel](https://github.com/holoviz/panel)
99
95
  - [ezmsg-blackrock](https://github.com/griffinmilsap/ezmsg-blackrock) -- Interface for Blackrock Cerebus ecosystem (incl. Neuroport) using `pycbsdk`
96
+ - [ezmsg-lsl](https://github.com/ezmsg-org/ezmsg-lsl) -- Source unit for LSL Inlet and sink unit for LSL Outlet
100
97
  - [ezmsg-unicorn](https://github.com/griffinmilsap/ezmsg-unicorn) -- g.tec Unicorn Hybrid Black integration for `ezmsg`
101
98
  - [ezmsg-gadget](https://github.com/griffinmilsap/ezmsg-gadget) -- USB-gadget with HID control integration for Raspberry Pi (Zero/W/2W, 4, CM4)
102
99
  - [ezmsg-openbci](https://github.com/griffinmilsap/ezmsg-openbci) -- OpenBCI Cyton serial interface for `ezmsg`
@@ -33,7 +33,7 @@ $ source env/bin/activate
33
33
  (env) $ python -m pytest tests # Optionally, Perform tests
34
34
  ```
35
35
 
36
- Note that it is generally recommended to install poetry into it's own standalone venv via the `pipx` cli tool.
36
+ Note that it is generally recommended to install poetry into its own standalone venv via the `pipx` cli tool.
37
37
 
38
38
  ## Documentation
39
39
 
@@ -50,7 +50,7 @@ pip install "ezmsg[all_ext]"
50
50
  ```
51
51
 
52
52
  This will install all the available public extension packages for `ezmsg` that are listed in `pyproject.toml`.
53
- If you prefer to install the extension packages individually, you can use the following command:
53
+ If you prefer to install a subset of extension packages, you can use the following command:
54
54
 
55
55
  ```bash
56
56
  pip install "ezmsg[zmq,sigproc,...]"
@@ -58,17 +58,14 @@ pip install "ezmsg[zmq,sigproc,...]"
58
58
 
59
59
  Please note that the `ezmsg` package itself can still be installed without any additional extensions using `pip install ezmsg`.
60
60
 
61
- See the extension directory for more details
62
-
63
- - `ezmsg-sigproc` -- Timeseries signal processing modules
64
- - `ezmsg-websocket` -- Websocket server and client nodes for `ezmsg` graphs
65
- - `ezmsg-zmq` -- ZeroMQ pub and sub nodes for `ezmsg` graphs
66
- - ... More to come!
67
-
68
- Additionally, the following extensions are contained in external repositories:
61
+ Extensions can be managed manually as well. Here are some of the extensions we manage or are aware of:
69
62
 
63
+ - [ezmsg-sigproc](https://github.com/ezmsg-org/ezmsg-sigproc) -- Timeseries signal processing modules
64
+ - [ezmsg-websocket](https://github.com/ezmsg-org/ezmsg-websocket) -- Websocket server and client nodes for `ezmsg` graphs
65
+ - [ezmsg-zmq](https://github.com/ezmsg-org/ezmsg-zmq) -- ZeroMQ pub and sub nodes for `ezmsg` graphs
70
66
  - [ezmsg-panel](https://github.com/griffinmilsap/ezmsg-panel) -- Plotting tools for `ezmsg` that use [panel](https://github.com/holoviz/panel)
71
67
  - [ezmsg-blackrock](https://github.com/griffinmilsap/ezmsg-blackrock) -- Interface for Blackrock Cerebus ecosystem (incl. Neuroport) using `pycbsdk`
68
+ - [ezmsg-lsl](https://github.com/ezmsg-org/ezmsg-lsl) -- Source unit for LSL Inlet and sink unit for LSL Outlet
72
69
  - [ezmsg-unicorn](https://github.com/griffinmilsap/ezmsg-unicorn) -- g.tec Unicorn Hybrid Black integration for `ezmsg`
73
70
  - [ezmsg-gadget](https://github.com/griffinmilsap/ezmsg-gadget) -- USB-gadget with HID control integration for Raspberry Pi (Zero/W/2W, 4, CM4)
74
71
  - [ezmsg-openbci](https://github.com/griffinmilsap/ezmsg-openbci) -- OpenBCI Cyton serial interface for `ezmsg`
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ezmsg"
3
- version = "3.3.4"
3
+ version = "3.4.0"
4
4
  description = "A simple DAG-based computation model"
5
5
  authors = [
6
6
  "Milsap, Griffin <griffin.milsap@gmail.com>",
@@ -39,6 +39,12 @@ pytest-cov = "*"
39
39
  numpy = "^1.24.2"
40
40
  flake8 = "*"
41
41
 
42
+
43
+ [tool.poetry.group.docs.dependencies]
44
+ sphinx = "<7.2"
45
+ sphinx-rtd-theme = "^2.0.0"
46
+ ezmsg-sigproc = { version = "*", source = "pypi" }
47
+
42
48
  [tool.poetry.extras]
43
49
  sigproc = ["ezmsg-sigproc"]
44
50
  websocket = ["ezmsg-websocket"]
@@ -167,6 +167,27 @@ def run(
167
167
  force_single_process: bool = False,
168
168
  **components_kwargs: Component,
169
169
  ) -> None:
170
+ """
171
+ Begin execution of a set of :obj:`Component` s.
172
+
173
+ `The old method` :obj:`run_system` `has been deprecated and uses` ``run()`` `instead.`
174
+
175
+ Args:
176
+ components: represents the nodes in the directed acyclic graph. It is a dictionary which contains the
177
+ ``Components`` to be run mapped to string names. On initialization, ``ezmsg`` will call ``initialize()``
178
+ for each :obj:`Unit` and ``configure()`` for each :obj:`Collection`, if defined.
179
+ root_name:
180
+ connections: represents the edges is a ``NetworkDefinition`` which connects
181
+ ``OutputStreams`` to ``InputStreams``. On initialization, ``ezmsg`` will create a directed acyclic graph
182
+ using the contents of this parameter.
183
+ process_components: a list of ``Components`` which should live in their own process.
184
+ backend_process: is currently under development.
185
+ graph_address: the hostname and port of the graph server which ``ezmsg`` should connect to.
186
+ If not defined, ``ezmsg`` will start a new graph server at 127.0.0.1:25978.
187
+ force_single_process: run all ``Components`` in one process.
188
+ This is necessary when running ``ezmsg`` in a notebook.
189
+ components_kwargs:
190
+ """
170
191
  # FIXME: This function is the last major re-implementation needed to make this
171
192
  # codebase more maintainable.
172
193
  graph_service = GraphService(graph_address)
@@ -41,10 +41,17 @@ logger = logging.getLogger("ezmsg")
41
41
 
42
42
 
43
43
  class Complete(Exception):
44
+ """
45
+ A type of ``Exception`` which signals to ``ezmsg`` that the function can be shut down gracefully.
46
+ If all functions in all :obj:`Units` raise ``Complete``, the entire pipeline will terminate execution.
47
+ """
44
48
  pass
45
49
 
46
50
 
47
51
  class NormalTermination(Exception):
52
+ """
53
+ A type of ``Exception`` which signals to ``ezmsg`` that the pipeline can be shut down gracefully.
54
+ """
48
55
  pass
49
56
 
50
57
 
@@ -294,32 +301,28 @@ class DefaultBackendProcess(BackendProcess):
294
301
  )
295
302
 
296
303
  pub_fn = perf_publish if hasattr(task, TIMEIT_ATTR) else publish
304
+
305
+ call_fn = lambda _: task(unit)
306
+ signature = inspect.signature(task)
307
+ if len(signature.parameters) == 1:
308
+ call_fn = lambda _: task(unit)
309
+ elif len(signature.parameters) == 2:
310
+ call_fn = lambda msg: task(unit, msg)
311
+ else:
312
+ logger.error(f'Incompatible call signature on task: {task.__name__}')
297
313
 
298
314
  @wraps(task)
299
315
  async def wrapped_task(msg: Any = None) -> None:
300
316
  try:
301
- # If we don't sub or pub anything, we are a simple task
302
- if not hasattr(task, SUBSCRIBES_ATTR) and not hasattr(
303
- task, PUBLISHES_ATTR
304
- ):
305
- await task(unit)
306
-
307
- # No subscriptions; only publications...
308
- elif not hasattr(task, SUBSCRIBES_ATTR):
309
- async for stream, obj in task(unit):
317
+ result = call_fn(msg)
318
+ if inspect.isasyncgen(result):
319
+ async for stream, obj in result:
320
+ if obj and getattr(task, ZERO_COPY_ATTR, False) and obj is msg:
321
+ obj = deepcopy(obj)
310
322
  await pub_fn(stream, obj)
311
323
 
312
- # Subscribers need to be called with a message
313
- else:
314
- if not getattr(task, ZERO_COPY_ATTR):
315
- msg = deepcopy(msg)
316
- if hasattr(task, PUBLISHES_ATTR):
317
- async for stream, obj in task(unit, msg):
318
- if getattr(task, ZERO_COPY_ATTR) and obj is msg:
319
- obj = deepcopy(obj)
320
- await pub_fn(stream, obj)
321
- else:
322
- await task(unit, msg)
324
+ elif asyncio.iscoroutine(result):
325
+ await result
323
326
 
324
327
  except Complete:
325
328
  logger.info(f"{task_address} Complete")
@@ -34,7 +34,9 @@ class CollectionMeta(ComponentMeta):
34
34
 
35
35
 
36
36
  class Collection(Component, metaclass=CollectionMeta):
37
- """Collections can contain subunits and connect them together"""
37
+ """
38
+ Connects :obj:`Unit` s together by defining a graph which connects :obj:`OutputStream` s to :obj:`InputStream` s.
39
+ """
38
40
 
39
41
  def __init__(self, *args, settings: typing.Optional[Settings] = None, **kwargs):
40
42
  super(Collection, self).__init__(*args, settings=settings, **kwargs)
@@ -44,11 +46,24 @@ class Collection(Component, metaclass=CollectionMeta):
44
46
  setattr(self, comp_name, comp)
45
47
 
46
48
  def configure(self) -> None:
47
- """This is where to percolate apply_settings to subnodes"""
49
+ """
50
+ A lifecycle hook that runs when the :obj:`Collection` is instantiated.
51
+ This is the best place to call ``Unit.apply_settings()`` on each member :obj:`Unit` of the :obj:`Collection`.
52
+ """
48
53
  ...
49
54
 
50
55
  def network(self) -> NetworkDefinition:
56
+ """
57
+ Override this method and have the definition return a :obj:`NetworkDefinition` which defines how
58
+ :obj:`InputStream` and :obj:`OutputStream` from member :obj:`Unit` s will be connected.
59
+ """
51
60
  return ()
52
61
 
53
62
  def process_components(self) -> typing.Collection[Component]:
63
+ """
64
+ Override this method and have the definition return a tuple which contains :obj:`Unit` and :obj:`Collection`
65
+ which should run in their own processes.
66
+
67
+ Return: the :obj:`Collection`.
68
+ """
54
69
  return (self,)
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import sys
2
3
  import base64
3
4
  import asyncio
4
5
  import argparse
@@ -97,7 +98,7 @@ async def run_command(cmd: str, graph_address: Address, shm_address: Address) ->
97
98
 
98
99
  elif cmd == "start":
99
100
  popen = subprocess.Popen(
100
- ["python", "-m", "ezmsg.core", "serve", f"--address={graph_address}"]
101
+ [sys.executable, "-m", "ezmsg.core", "serve", f"--address={graph_address}"]
101
102
  )
102
103
 
103
104
  while True:
@@ -72,6 +72,10 @@ class ComponentMeta(ABCMeta):
72
72
 
73
73
 
74
74
  class Component(Addressable, metaclass=ComponentMeta):
75
+ """
76
+ Metaclass which :obj:`Unit` and :obj:`Collection` inherit from.
77
+ """
78
+
75
79
  SETTINGS: Settings
76
80
  STATE: State
77
81
 
@@ -96,21 +100,21 @@ class Component(Addressable, metaclass=ComponentMeta):
96
100
  for stream_name, stream in self.streams.items():
97
101
  setattr(self, stream_name, stream)
98
102
 
99
- try:
100
- if settings is None:
101
- # settings not supplied as a kwarg. Try to build it.
102
- if len(args) > 0 and type(args[0]) == self.__class__.__settings_type__:
103
- settings = args[0]
104
- elif len(args) > 0 or len(kwargs) > 0:
105
- settings = self.__class__.__settings_type__(*args, **kwargs)
106
- else:
103
+ if settings is None:
104
+ # settings not supplied as a kwarg. Try to build it.
105
+ if len(args) > 0 and type(args[0]) == self.__class__.__settings_type__:
106
+ settings = args[0]
107
+ elif len(args) > 0 or len(kwargs) > 0:
108
+ settings = self.__class__.__settings_type__(*args, **kwargs)
109
+ else:
110
+ try:
107
111
  # If we weren't supplied settings, we will try to
108
112
  # instantiate the settings type from annotations
109
113
  settings = self.__class__.__settings_type__()
110
- except TypeError:
111
- # We couldn't instantiate settings with default value
112
- # We will rely on late configuration via apply_settings
113
- pass
114
+ except TypeError:
115
+ # We couldn't instantiate settings with default value
116
+ # We will rely on late configuration via apply_settings
117
+ pass
114
118
 
115
119
  if settings is not None:
116
120
  self.apply_settings(settings)
@@ -132,6 +136,13 @@ class Component(Addressable, metaclass=ComponentMeta):
132
136
  )
133
137
 
134
138
  def apply_settings(self, settings: Settings) -> None:
139
+ """
140
+ Update the ``Component``‘s ``Settings`` object.
141
+
142
+ Args:
143
+ settings: An instance of the class-specific ``Settings``.
144
+
145
+ """
135
146
  self.SETTINGS = settings
136
147
  self._settings_applied = True
137
148
 
@@ -0,0 +1,65 @@
1
+ import sys
2
+ import typing
3
+
4
+ from abc import ABC, ABCMeta
5
+ from dataclasses import dataclass
6
+
7
+ if sys.version_info < (3, 12):
8
+ from typing_extensions import dataclass_transform
9
+ else:
10
+ from typing import dataclass_transform
11
+
12
+ # All settings classes are dataclasses
13
+ # https://rednafi.github.io/digressions/python/2020/06/26/python-metaclasses.html
14
+ # see -- #avoiding-dataclass-decorator-with-metaclasses
15
+
16
+
17
+ @dataclass_transform()
18
+ class SettingsMeta(ABCMeta):
19
+ def __new__(
20
+ cls,
21
+ name: str,
22
+ bases: typing.Tuple[type, ...],
23
+ classdict: typing.Dict[str, typing.Any],
24
+ **kwargs: typing.Any
25
+ ) -> typing.Type["Settings"]:
26
+ new_cls = super().__new__(cls, name, bases, classdict)
27
+ return dataclass(frozen=True)(new_cls) # type: ignore
28
+
29
+
30
+ class Settings(ABC, metaclass=SettingsMeta):
31
+ """
32
+ To pass parameters into a :obj:`Component`, inherit from ``Settings``.
33
+
34
+ .. code-block:: python
35
+
36
+ class YourSettings(Settings):
37
+ setting1: int
38
+ setting2: float
39
+
40
+ To use, declare the ``Settings`` object for a ``Component`` as a member variable called (all-caps!) ``SETTINGS``. ``ezmsg`` will monitor the variable called ``SETTINGS`` in the background, so it is important to name it correctly.
41
+
42
+ .. code-block:: python
43
+
44
+ class YourUnit(Unit):
45
+
46
+ SETTINGS: YourSettings
47
+
48
+ A ``Unit`` can accept a ``Settings`` object as a parameter on instantiation.
49
+
50
+ .. code-block:: python
51
+
52
+ class YourCollection(Collection):
53
+
54
+ YOUR_UNIT = YourUnit(
55
+ YourSettings(
56
+ setting1: int,
57
+ setting2: float
58
+ )
59
+ )
60
+
61
+ .. note::
62
+ ``Settings`` uses type hints to define member variables, but does not enforce type checking.
63
+
64
+ """
65
+ ...
@@ -0,0 +1,56 @@
1
+ from abc import ABC, ABCMeta
2
+ from dataclasses import dataclass
3
+
4
+ from typing import (
5
+ Dict,
6
+ Tuple,
7
+ Any,
8
+ Type,
9
+ )
10
+
11
+
12
+ class StateMeta(ABCMeta):
13
+ def __new__(
14
+ cls,
15
+ name: str,
16
+ bases: Tuple[type, ...],
17
+ classdict: Dict[str, Any],
18
+ **kwargs: Any
19
+ ) -> Type["State"]:
20
+ new_cls = super().__new__(cls, name, bases, classdict)
21
+ return dataclass(unsafe_hash=True, frozen=False, init=False)(new_cls) # type: ignore
22
+
23
+
24
+ class State(ABC, metaclass=StateMeta):
25
+ """
26
+ States are mutable dataclasses that are instantiated by the Unit in its home process.
27
+
28
+ To track a mutable state for a ``Component``, inherit from ``State``.
29
+
30
+ .. code-block:: python
31
+
32
+ class YourState(State):
33
+ state1: int
34
+ state2: float
35
+
36
+ To use, declare the ``State`` object for a ``Component`` as a member variable called (all-caps!) ``STATE``.
37
+ ``ezmsg`` will monitor the variable called ``STATE`` in the background, so it is important to name it correctly.
38
+
39
+ Member functions can then access and mutate ``STATE`` as needed during function execution.
40
+ It is recommended to initialize state values inside the ``initialize()`` or ``configure()`` lifecycle hooks if
41
+ defaults are not defined.
42
+
43
+ .. code-block:: python
44
+
45
+ class YourUnit(Unit):
46
+
47
+ STATE: YourState
48
+
49
+ def initialize(self):
50
+ this.STATE.state1 = 0
51
+ this.STATE.state2 = 0.0
52
+
53
+ .. note::
54
+ ``State`` uses type hints to define member variables, but does not enforce type checking.
55
+ """
56
+ ...
@@ -5,6 +5,10 @@ from .addressable import Addressable
5
5
 
6
6
 
7
7
  class Stream(Addressable):
8
+ """
9
+
10
+ """
11
+
8
12
  msg_type: Type
9
13
 
10
14
  def __init__(self, msg_type: Type):
@@ -17,11 +21,17 @@ class Stream(Addressable):
17
21
 
18
22
 
19
23
  class InputStream(Stream):
24
+ """
25
+ Can be added to any ``Component`` as a member variable. Methods may subscribe to it.
26
+ """
20
27
  def __repr__(self) -> str:
21
28
  return f"Input{super().__repr__()}()"
22
29
 
23
30
 
24
31
  class OutputStream(Stream):
32
+ """
33
+ Can be added to any ``Component`` as a member variable. Methods may publish to it.
34
+ """
25
35
  host: Optional[str]
26
36
  port: Optional[int]
27
37
  num_buffers: int
@@ -59,7 +59,11 @@ class UnitMeta(ComponentMeta):
59
59
 
60
60
 
61
61
  class Unit(Component, metaclass=UnitMeta):
62
- """Units can subscribe, publish, and have tasks"""
62
+ """
63
+ Represents a single step in the graph.
64
+ Units can subscribe, publish, and have tasks.
65
+ To create a ``Unit``, inherit from the ``Unit`` class.
66
+ """
63
67
 
64
68
  def __init__(self, *args, settings: Optional[Settings] = None, **kwargs):
65
69
  super(Unit, self).__init__(*args, settings=settings, **kwargs)
@@ -85,16 +89,42 @@ class Unit(Component, metaclass=UnitMeta):
85
89
  self._check_state()
86
90
 
87
91
  async def initialize(self) -> None:
88
- """This is called from within the same process this unit will live"""
92
+ """
93
+ Runs when the ``Unit`` is instantiated.
94
+ This is called from within the same process this unit will live.
95
+ This lifecycle hook can be overridden. It can be run as ``async`` functions by simply adding the
96
+ ``async`` keyword when overriding.
97
+ """
89
98
  pass
90
99
 
91
100
  async def shutdown(self) -> None:
92
- """This is called from within the same process this unit will live"""
101
+ """
102
+ Runs when the ``Unit`` terminates.
103
+ This is called from within the same process this unit will live.
104
+ This lifecycle hook can be overridden. It can be run as ``async`` functions by simply adding the
105
+ ``async`` keyword when overriding.
106
+ """
93
107
  pass
94
108
 
95
109
 
96
110
  def publisher(stream: OutputStream):
97
- """A decorator for a method that publishes to a stream in the task/messaging thread"""
111
+ """
112
+ A decorator for a method that publishes to a stream in the task/messaging thread.
113
+ An async function will yield messages on the designated :obj:`OutputStream`.
114
+
115
+ .. code-block:: python
116
+
117
+ from typing import AsyncGenerator
118
+
119
+ OUTPUT = OutputStream(ez.Message)
120
+
121
+ @publisher(OUTPUT)
122
+ async def send_message(self) -> AsyncGenerator:
123
+ message = Message()
124
+ yield(OUTPUT, message)
125
+
126
+ A function can have both ``@subscriber`` and ``@publisher`` decorators.
127
+ """
98
128
 
99
129
  if not isinstance(stream, OutputStream):
100
130
  raise ValueError(f"Cannot publish to object of type {type(stream)}")
@@ -109,7 +139,22 @@ def publisher(stream: OutputStream):
109
139
 
110
140
 
111
141
  def subscriber(stream: InputStream, zero_copy: bool = False):
112
- """A decorator for a method that subscribes to a stream in the task/messaging thread"""
142
+ """
143
+ A decorator for a method that subscribes to a stream in the task/messaging thread.
144
+ An async function will run once per message received from the :obj:`InputStream` it subscribes to.
145
+
146
+ Example:
147
+
148
+ .. code-block:: python
149
+
150
+ INPUT = ez.InputStream(Message)
151
+
152
+ @subscriber(INPUT)
153
+ async def print_message(self, message: Message) -> None:
154
+ print(message)
155
+
156
+ A function can have both ``@subscriber`` and ``@publisher`` decorators.
157
+ """
113
158
 
114
159
  if not isinstance(stream, InputStream):
115
160
  raise ValueError(f"Cannot subscribe to object of type {type(stream)}")
@@ -126,12 +171,18 @@ def subscriber(stream: InputStream, zero_copy: bool = False):
126
171
 
127
172
 
128
173
  def main(func: Callable):
129
- """A decorator for a function that runs as the main thread. A Unit may only have one of these."""
174
+ """
175
+ A decorator which designates this function to run as the main thread for this :obj:`Unit`.
176
+ A :obj:`Unit` may only have one of these.
177
+ """
130
178
  setattr(func, MAIN_ATTR, True)
131
179
  return func
132
180
 
133
181
 
134
182
  def timeit(func: Callable):
183
+ """
184
+ ``ezmsg`` will log the amount of time this function takes to execute.
185
+ """
135
186
  setattr(func, TIMEIT_ATTR, True)
136
187
 
137
188
  @functools.wraps(func)
@@ -148,17 +199,24 @@ def timeit(func: Callable):
148
199
 
149
200
 
150
201
  def thread(func: Callable):
151
- """A decorator for a function that runs in a background thread"""
202
+ """
203
+ A decorator which designates this function to run as a background thread for this `:obj:`Unit`.
204
+ """
152
205
  setattr(func, THREAD_ATTR, True)
153
206
  return func
154
207
 
155
208
 
156
209
  def task(func: Callable):
157
- """A decorator for a function that runs as a task in the task/messaging thread"""
210
+ """
211
+ A decorator which designates this function to run as a task in the task/messaging thread.
212
+ """
158
213
  setattr(func, TASK_ATTR, True)
159
214
  return func
160
215
 
161
216
 
162
217
  def process(func: Callable):
218
+ """
219
+ A decorator which designates this function to run in its own process.
220
+ """
163
221
  setattr(func, PROCESS_ATTR, True)
164
222
  return func
@@ -4,15 +4,31 @@ from typing import AsyncGenerator, Optional, Any
4
4
 
5
5
 
6
6
  class DebugLogSettings(ez.Settings):
7
- name: str = "DEBUG" # Useful name for logger
8
- max_length: Optional[int] = 400 # No limit if `None``
7
+ """
8
+ ``Settings`` class associated with :obj:`DebugLog`
9
+
10
+ Args:
11
+ name: Useful name for the logger. The name is included in the logstring so that if multiple DebugLogs
12
+ are used in one pipeline, their messages can be differentiated.
13
+ max_length: Sets a maximum number of chars which will be printed from the message.
14
+ If the message is longer, the log message will be truncated.
15
+ """
16
+ name: str = "DEBUG"
17
+ max_length: Optional[int] = 400
9
18
 
10
19
 
11
20
  class DebugLog(ez.Unit):
21
+ """
22
+ Logs messages that pass through.
23
+ """
24
+
12
25
  SETTINGS: DebugLogSettings
13
26
 
14
27
  INPUT = ez.InputStream(Any)
28
+ """Send messages to log here."""
29
+
15
30
  OUTPUT = ez.OutputStream(Any)
31
+ """Send messages back out to continue through the graph."""
16
32
 
17
33
  @ez.subscriber(INPUT, zero_copy=True)
18
34
  @ez.publisher(OUTPUT)
@@ -94,6 +94,7 @@ class GenAxisArray(ez.Unit):
94
94
 
95
95
  INPUT_SIGNAL = ez.InputStream(AxisArray)
96
96
  OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
97
+ INPUT_SETTINGS = ez.InputStream(ez.Settings)
97
98
 
98
99
  def initialize(self) -> None:
99
100
  self.construct_generator()
@@ -102,6 +103,11 @@ class GenAxisArray(ez.Unit):
102
103
  def construct_generator(self):
103
104
  raise NotImplementedError
104
105
 
106
+ @ez.subscriber(INPUT_SETTINGS)
107
+ async def on_settings(self, msg: ez.Settings) -> None:
108
+ self.apply_settings(msg)
109
+ self.construct_generator()
110
+
105
111
  @ez.subscriber(INPUT_SIGNAL)
106
112
  @ez.publisher(OUTPUT_SIGNAL)
107
113
  async def on_message(self, message: AxisArray) -> AsyncGenerator:
@@ -6,14 +6,23 @@ import ezmsg.core as ez
6
6
 
7
7
  @dataclass
8
8
  class GateMessage:
9
+ """Send this message to ``INPUT_GATE`` to open or close the gate."""
9
10
  open: bool
10
11
 
11
12
 
12
13
  class MessageGateSettings(ez.Settings):
14
+ """
15
+ Settings for :obj:`MessageGate` unit.
16
+
17
+ Args:
18
+ start_open: sets the gate's initial state to allow messages to flow through or be discarded. ``True`` will
19
+ allow messages to flow through initially, ``False`` will discard messages initially.
20
+ default_open: sets the gate's behavior after the `default_after` number of messages have flowed through.
21
+ ``True`` will allow messages to flow through, ``False`` will discard messages.
22
+ default_after: sets the number of messages after which the `default_open` state will be applied.
23
+ """
13
24
  start_open: bool = False
14
25
  default_open: bool = False
15
-
16
- # Automatically change back to default state after X messages
17
26
  default_after: typing.Optional[int] = None
18
27
 
19
28
 
@@ -23,13 +32,25 @@ class MessageGateState(ez.State):
23
32
 
24
33
 
25
34
  class MessageGate(ez.Unit):
35
+ """
36
+ Blocks ``Messages`` from continuing through the system.
37
+ Can be set as open, closed, open after n messages, or closed after n messages.
38
+ """
39
+
26
40
  SETTINGS: MessageGateSettings
27
41
  STATE: MessageGateState
28
42
 
29
43
  INPUT_GATE = ez.InputStream(GateMessage)
44
+ """
45
+ Stop or start message flow. If ``GateMessage.open == True``, messages will flow through.
46
+ If ``GateMessage.open == False``, messages will be discarded.
47
+ """
30
48
 
31
49
  INPUT = ez.InputStream(typing.Any)
50
+ """Messages which will flow through or be discarded, depending on gate status."""
51
+
32
52
  OUTPUT = ez.OutputStream(typing.Any)
53
+ """Publishes messages which flow through."""
33
54
 
34
55
  def initialize(self) -> None:
35
56
  self.STATE.gate_open = self.SETTINGS.start_open
@@ -17,6 +17,13 @@ def log_object(obj: Any) -> str:
17
17
 
18
18
 
19
19
  class MessageLoggerSettings(ez.Settings):
20
+ """
21
+ Settings for :obj:`MessageLogger` Unit.
22
+
23
+ Args:
24
+ output: :py:class:`pathlib.Path` for a file where the messages will be logged.
25
+ If the file path already exists, the existing file will be truncated to 0 length.
26
+ """
20
27
  output: Optional[Path] = None
21
28
 
22
29
 
@@ -25,15 +32,41 @@ class MessageLoggerState(ez.State):
25
32
 
26
33
 
27
34
  class MessageLogger(ez.Unit):
35
+ """
36
+ Logs all messages it receives to a file.
37
+ File path can be set in ``SETTINGS`` or set dynamically by passing a
38
+ :py:class:`pathlib.Path` to ``INPUT_START``.
39
+ """
40
+
28
41
  SETTINGS: MessageLoggerSettings
29
42
  STATE: MessageLoggerState
30
43
 
31
44
  INPUT_START = ez.InputStream(Path)
45
+ """
46
+ Pass a :py:class:`pathlib.Path`
47
+ to begin logging messages to that path. If the file path already exists, the existing
48
+ file will be truncated to 0 length. If the file is already open, nothing will happen.
49
+ """
50
+
32
51
  INPUT_STOP = ez.InputStream(Path)
52
+ """
53
+ Pass a :py:class:`pathlib.Path`
54
+ to stop logging messages to that path.
55
+ """
56
+
33
57
  INPUT_MESSAGE = ez.InputStream(Any)
58
+ """Pass a piece of data to log it to every open file which the ``MessageLogger`` is using."""
59
+
34
60
  OUTPUT_MESSAGE = ez.OutputStream(Any)
61
+ """Messages which are sent to ``INPUT_MESSAGE`` will pass through and be published on ``OUTPUT_MESSAGE``."""
62
+
35
63
  OUTPUT_START = ez.OutputStream(Path)
64
+ """If a file passed to ``INPUT_START`` is successfully opened, its path will be published to
65
+ ``OUTPUT_START``, otherwise ``None``."""
66
+
36
67
  OUTPUT_STOP = ez.OutputStream(Path)
68
+ """If a file passed to ``INPUT_STOP`` is successfully closed, its path will be published to
69
+ ``OUTPUT_STOP``, otherwise ``None``."""
37
70
 
38
71
  def open_file(self, filepath: Path) -> Optional[Path]:
39
72
  """Returns file path if file successfully opened, otherwise None"""
@@ -5,6 +5,13 @@ from ezmsg.util.rate import Rate
5
5
 
6
6
 
7
7
  class MessageQueueSettings(ez.Settings):
8
+ """
9
+ Settings for :obj:`MessageQueue` class.
10
+
11
+ Args:
12
+ maxsize: The maximum number of items which the queue will hold.
13
+ leaky: Whether the queue will drop new messages when it reaches its maxsize, or whether it will wait for space to open for them.
14
+ """
8
15
  maxsize: int = 0
9
16
  leaky: bool = False
10
17
  log_above_n: Optional[int] = None
@@ -17,11 +24,18 @@ class MessageQueueState(ez.State):
17
24
 
18
25
 
19
26
  class MessageQueue(ez.Unit):
27
+ """
28
+ Place between two other ``Units`` to induce backpressure.
29
+ """
30
+
20
31
  SETTINGS: MessageQueueSettings
21
32
  STATE: MessageQueueState
22
33
 
23
34
  INPUT = ez.InputStream(Any)
35
+ """Send messages to queue here."""
36
+
24
37
  OUTPUT = ez.OutputStream(Any)
38
+ """Subscribe to pull messages out of the queue."""
25
39
 
26
40
  def initialize(self):
27
41
  self.STATE.leaky = self.SETTINGS.leaky
@@ -13,6 +13,15 @@ from .messagecodec import MessageDecoder, LogStart
13
13
 
14
14
  @dataclass
15
15
  class ReplayStatusMessage:
16
+ """
17
+ Message which gives the status of a file replay.
18
+
19
+ Args:
20
+ filename: The name of the file currently being replayed.
21
+ idx: The line number of the message that was just published.
22
+ total: Number of messages in the file.
23
+ done: Whether the file has finished replaying.
24
+ """
16
25
  filename: Path
17
26
  idx: int
18
27
  total: int
@@ -21,13 +30,26 @@ class ReplayStatusMessage:
21
30
 
22
31
  @dataclass
23
32
  class FileReplayMessage:
33
+ """
34
+ Add a file to the queue.
35
+
36
+ Args:
37
+ filename: The path of the file to replay.
38
+ rate: in Hertz at which the messages will be published.
39
+ 0 = realtime (if timestamps in file)
40
+ If not specified, messages will publish as fast as possible.
41
+ """
24
42
  filename: typing.Optional[Path] = None
25
-
26
- # 0 = realtime (if timestamps in file), None = as fast as possible
27
43
  rate: typing.Optional[float] = None # Hz
28
44
 
29
45
 
30
46
  class MessageReplaySettings(ez.Settings, FileReplayMessage):
47
+ """
48
+ Settings for :obj:`MesssageReplay` Unit.
49
+
50
+ Args:
51
+ progress: will use tqdm to indicate progress through the file. tqdm must be installed.
52
+ """
31
53
  progress: bool = False
32
54
 
33
55
 
@@ -38,17 +60,37 @@ class MessageReplayState(ez.State):
38
60
 
39
61
 
40
62
  class MessageReplay(ez.Unit):
63
+ """
64
+ Stream messages from files created by :obj:`MessageLogger`.
65
+ Stores a queue of files to stream and streams from them in order.
66
+ """
67
+
41
68
  SETTINGS: MessageReplaySettings
42
69
  STATE: MessageReplayState
43
70
 
44
71
  INPUT_FILE = ez.InputStream(FileReplayMessage)
45
- INPUT_PAUSED = ez.InputStream(bool) # Pause state; True = paused, False = running
46
- INPUT_STOP = ez.InputStream(bool) # True = clear queue
72
+ """Add a new file to the queue."""
73
+
74
+ INPUT_PAUSED = ez.InputStream(bool)
75
+ """Send ``True`` to pause the stream, ``False`` to restart the stream."""
76
+
77
+ INPUT_STOP = ez.InputStream(bool)
78
+ """
79
+ Stop the stream. Send ``True`` to also clear the queue.
80
+ Send ``False`` to reset to the beginning of the current file.
81
+ """
47
82
 
48
83
  OUTPUT_MESSAGE = ez.OutputStream(typing.Any)
84
+ """The output on which the messages from the files will be streamed."""
85
+
49
86
  OUTPUT_TOTAL = ez.OutputStream(int)
87
+ """
88
+ Publishes an integer total of messages which have been published on OUTPUT_MESSAGE from a single file.
89
+ Resets when a file completes.
90
+ """
50
91
 
51
92
  OUTPUT_REPLAY_STATUS = ez.OutputStream(ReplayStatusMessage)
93
+ """Publishes status messages."""
52
94
 
53
95
  async def initialize(self) -> None:
54
96
  self.STATE.replay_files = asyncio.Queue()
@@ -167,10 +209,17 @@ class MessageCollectorState(ez.State):
167
209
 
168
210
 
169
211
  class MessageCollector(ez.Unit):
212
+ """
213
+ Collects ``Messages`` into a local list.
214
+ """
215
+
170
216
  STATE: MessageCollectorState
171
217
 
172
218
  INPUT_MESSAGE = ez.InputStream(typing.Any)
219
+ """Send messages here to be collected."""
220
+
173
221
  OUTPUT_MESSAGE = ez.OutputStream(typing.Any)
222
+ """Messages will pass straight through after being recorded and be published here."""
174
223
 
175
224
  @ez.subscriber(INPUT_MESSAGE)
176
225
  @ez.publisher(OUTPUT_MESSAGE)
@@ -180,4 +229,9 @@ class MessageCollector(ez.Unit):
180
229
 
181
230
  @property
182
231
  def messages(self) -> typing.List[typing.Any]:
232
+ """
233
+ Access the list of messages.
234
+
235
+ :return: A list of messages which have been collected.
236
+ """
183
237
  return self.STATE.messages
@@ -12,10 +12,14 @@ import numpy.lib.stride_tricks as nps
12
12
  from ezmsg.core.util import either_dict_or_kwargs
13
13
 
14
14
  # TODO: Typehinting is all wrong in this and
15
- # concatenate/transpose should probably not be staticmethods
15
+ # concatenate/transpose should probably not be staticmethods
16
+
16
17
 
17
18
  @dataclass
18
19
  class AxisArray:
20
+ """
21
+ A lightweight message class comprising a numpy ndarray and its metadata.
22
+ """
19
23
  data: npt.NDArray
20
24
  dims: typing.List[str]
21
25
  axes: typing.Dict[str, "AxisArray.Axis"] = field(default_factory=dict)
@@ -206,22 +210,24 @@ class AxisArray:
206
210
  def slice_along_axis(in_arr: npt.NDArray, sl: typing.Union[slice, int], axis: int) -> npt.NDArray:
207
211
  """
208
212
  Slice the input array along a specified axis using the given slice object or integer index.
213
+ Integer arguments to `sl` will cause the sliced dimension to be dropped.
214
+ Use `slice(my_int, my_int+1, None)` to keep the sliced dimension.
209
215
 
210
216
  Parameters:
211
- in_arr (npt.NDArray): The input array to be sliced.
212
- sl (Union[slice, int]): The slice object or integer index to use for slicing.
213
- axis (int): The axis along which to slice the array.
217
+ in_arr: The input array to be sliced.
218
+ sl: The slice object or integer index to use for slicing.
219
+ axis: The axis along which to slice the array.
214
220
 
215
221
  Returns:
216
- npt.NDArray: The sliced array (view).
222
+ The sliced array (view).
217
223
 
218
224
  Raises:
219
225
  ValueError: If the axis value is invalid for the input array.
220
226
  """
221
227
  if axis < -in_arr.ndim or axis >= in_arr.ndim:
222
228
  raise ValueError(f"Invalid axis value {axis} for input array with {in_arr.ndim} dimensions.")
223
- if axis < 0:
224
- axis = in_arr.ndim + axis
229
+ if -in_arr.ndim <= axis < 0:
230
+ axis = in_arr.ndim + axis
225
231
  all_slice = (slice(None),) * axis + (sl,) + (slice(None),) * (in_arr.ndim - axis - 1)
226
232
  return in_arr[all_slice]
227
233
 
@@ -229,23 +235,36 @@ def slice_along_axis(in_arr: npt.NDArray, sl: typing.Union[slice, int], axis: in
229
235
  def sliding_win_oneaxis(in_arr: npt.NDArray, nwin: int, axis: int) -> npt.NDArray:
230
236
  """
231
237
  Generates a view of an array using a sliding window of specified length along a specified axis of the input array.
232
- This is a slightly optimized version of nps.sliding_window_view with a few important differences.
233
- Because we only accept a single nwin and a single axis, we can skip some checks.
234
- The new `win` axis precedes immediately the original target axis, unlike sliding_window_view where the
235
- target axis is moved to the end of the output.
238
+ This is a slightly optimized version of nps.sliding_window_view with a few important differences:
236
239
 
237
- Parameters:
238
- in_arr (npt.NDArray): The input array.
239
- nwin (int): The size of the sliding window.
240
- axis (int): The axis along which the sliding window will be applied.
240
+ - This only accepts a single nwin and a single axis, thus we can skip some checks.
241
+ - The new `win` axis precedes immediately the original target axis, unlike sliding_window_view where the
242
+ target axis is moved to the end of the output.
243
+
244
+ Combine this with slice_along_axis(..., sl=slice(None, None, step), axis=axis) to step the window
245
+ by more than 1 sample at a time.
246
+
247
+ Args:
248
+ in_arr: The input array.
249
+ nwin: The size of the sliding window.
250
+ axis: The axis along which the sliding window will be applied.
241
251
 
242
252
  Returns:
243
- npt.NDArray: A view to the input array with the sliding window applied.
253
+ A view to the input array with the sliding window applied.
254
+
255
+ Note: There is a known edge case when nwin == shape[axis] + 1. While this should raise
256
+ an error because the window is larger than the input, the implementation ends up
257
+ returning a 0-length window. We could check for this but this function is intended
258
+ to have minimal latency so we have decided to skip the checks and deal with the
259
+ support issues as they arise.
244
260
  """
261
+ if -in_arr.ndim <= axis < 0:
262
+ axis = in_arr.ndim + axis
245
263
  out_strides = in_arr.strides[:axis] + (in_arr.strides[axis],) * 2 + in_arr.strides[axis+1:]
246
264
  out_shape = in_arr.shape[:axis] + (in_arr.shape[axis]-(nwin-1),) + (nwin,) + in_arr.shape[axis+1:]
247
265
  return nps.as_strided(in_arr, strides=out_strides, shape=out_shape, writeable=False)
248
266
 
267
+
249
268
  def _as2d(
250
269
  in_arr: npt.NDArray, axis: int = 0
251
270
  ) -> typing.Tuple[npt.NDArray, typing.Tuple[int]]:
@@ -9,8 +9,15 @@ from typing import Optional, Any
9
9
 
10
10
 
11
11
  class TerminateOnTimeoutSettings(ez.Settings):
12
- time: float = 2.0 # Terminate if no message has been received in this time (sec)
13
- poll_rate: float = 4.0 # Probably no good reason to mess with this (Hz)
12
+ """
13
+ Settings for :obj:`TerminateOnTimeout` Unit.
14
+
15
+ Args:
16
+ time: Terminate if no message has been received in this time (sec)
17
+ poll_rate: Hz.
18
+ """
19
+ time: float = 2.0
20
+ poll_rate: float = 4.0
14
21
 
15
22
 
16
23
  class TerminateOnTimeoutState(ez.State):
@@ -18,10 +25,15 @@ class TerminateOnTimeoutState(ez.State):
18
25
 
19
26
 
20
27
  class TerminateOnTimeout(ez.Unit):
28
+ """
29
+ End a pipeline execution when a certain amount of time has passed without receiving a message.
30
+ """
31
+
21
32
  SETTINGS: TerminateOnTimeoutSettings
22
33
  STATE: TerminateOnTimeoutState
23
34
 
24
35
  INPUT = ez.InputStream(Any)
36
+ """Send messages here."""
25
37
 
26
38
  @ez.subscriber(INPUT)
27
39
  async def keepalive(self, _: Any) -> None:
@@ -40,6 +52,12 @@ class TerminateOnTimeout(ez.Unit):
40
52
 
41
53
 
42
54
  class TerminateOnTotalSettings(ez.Settings):
55
+ """
56
+ Settings for :obj:`TerminateOnTotal` Unit.
57
+
58
+ Args:
59
+ total: The total number of messages to terminate after.
60
+ """
43
61
  total: Optional[int] = None
44
62
 
45
63
 
@@ -49,11 +67,21 @@ class TerminateOnTotalState(ez.State):
49
67
 
50
68
 
51
69
  class TerminateOnTotal(ez.Unit):
70
+ """
71
+ End a pipeline execution once a certain number of messages have been received.
72
+ """
73
+
52
74
  SETTINGS: TerminateOnTotalSettings
53
75
  STATE: TerminateOnTotalState
54
76
 
55
77
  INPUT_MESSAGE = ez.InputStream(Any)
78
+ """Send messages here."""
79
+
56
80
  INPUT_TOTAL = ez.InputStream(int)
81
+ """
82
+ Change the total number of messages to terminate after.
83
+ If this number has already been reached, termination will occur immediately.
84
+ """
57
85
 
58
86
  def initialize(self) -> None:
59
87
  self.STATE.total = self.SETTINGS.total
@@ -1,31 +0,0 @@
1
- import sys
2
- import typing
3
-
4
- from abc import ABC, ABCMeta
5
- from dataclasses import dataclass
6
-
7
- if sys.version_info < (3, 12):
8
- from typing_extensions import dataclass_transform
9
- else:
10
- from typing import dataclass_transform
11
-
12
- # All settings classes are dataclasses
13
- # https://rednafi.github.io/digressions/python/2020/06/26/python-metaclasses.html
14
- # see -- #avoiding-dataclass-decorator-with-metaclasses
15
-
16
-
17
- @dataclass_transform()
18
- class SettingsMeta(ABCMeta):
19
- def __new__(
20
- cls,
21
- name: str,
22
- bases: typing.Tuple[type, ...],
23
- classdict: typing.Dict[str, typing.Any],
24
- **kwargs: typing.Any
25
- ) -> typing.Type["Settings"]:
26
- new_cls = super().__new__(cls, name, bases, classdict)
27
- return dataclass(frozen=True)(new_cls) # type: ignore
28
-
29
-
30
- class Settings(ABC, metaclass=SettingsMeta):
31
- ...
@@ -1,29 +0,0 @@
1
- from abc import ABC, ABCMeta
2
- from dataclasses import dataclass
3
-
4
- from typing import (
5
- Dict,
6
- Tuple,
7
- Any,
8
- Type,
9
- )
10
-
11
-
12
- class StateMeta(ABCMeta):
13
- def __new__(
14
- cls,
15
- name: str,
16
- bases: Tuple[type, ...],
17
- classdict: Dict[str, Any],
18
- **kwargs: Any
19
- ) -> Type["State"]:
20
- new_cls = super().__new__(cls, name, bases, classdict)
21
- return dataclass(unsafe_hash=True, frozen=False, init=False)(new_cls) # type: ignore
22
-
23
-
24
- class State(ABC, metaclass=StateMeta):
25
- """
26
- States are mutable dataclasses that are instantiated by the Unit in its home process.
27
- """
28
-
29
- ...
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes