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.
- {ezmsg-3.3.4 → ezmsg-3.4.0}/PKG-INFO +8 -11
- {ezmsg-3.3.4 → ezmsg-3.4.0}/README.md +7 -10
- {ezmsg-3.3.4 → ezmsg-3.4.0}/pyproject.toml +7 -1
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/backend.py +21 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/backendprocess.py +23 -20
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/collection.py +17 -2
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/command.py +2 -1
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/component.py +23 -12
- ezmsg-3.4.0/src/ezmsg/core/settings.py +65 -0
- ezmsg-3.4.0/src/ezmsg/core/state.py +56 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/stream.py +10 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/unit.py +66 -8
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/debuglog.py +18 -2
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/generator.py +6 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagegate.py +23 -2
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagelogger.py +33 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagequeue.py +14 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagereplay.py +58 -4
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messages/axisarray.py +35 -16
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/terminate.py +30 -2
- ezmsg-3.3.4/src/ezmsg/core/settings.py +0 -31
- ezmsg-3.3.4/src/ezmsg/core/state.py +0 -29
- {ezmsg-3.3.4 → ezmsg-3.4.0}/LICENSE +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/__init__.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/__main__.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/addressable.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/backpressure.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/dag.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/graphcontext.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/graphserver.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/message.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/messagecache.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/messagemarshal.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/netprotocol.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/pubclient.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/server.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/shmserver.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/subclient.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/core/util.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/__init__.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/gen_to_unit.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messagecodec.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messages/__init__.py +0 -0
- {ezmsg-3.3.4 → ezmsg-3.4.0}/src/ezmsg/util/messages/modify.py +0 -0
- {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
|
+
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
302
|
-
if
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
[
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
#
|
|
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
|
|
212
|
-
sl
|
|
213
|
-
axis
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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
|