ezmsg 3.3.3__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.4.0/PKG-INFO +114 -0
- ezmsg-3.4.0/README.md +85 -0
- ezmsg-3.4.0/pyproject.toml +79 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/__init__.py +6 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/backend.py +31 -6
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/backendprocess.py +24 -20
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/collection.py +19 -4
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/command.py +114 -6
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/component.py +27 -10
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/graphcontext.py +3 -1
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/graphserver.py +15 -11
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/message.py +8 -2
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/messagecache.py +1 -1
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/server.py +1 -1
- ezmsg-3.4.0/src/ezmsg/core/settings.py +65 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/shmserver.py +11 -1
- ezmsg-3.4.0/src/ezmsg/core/state.py +56 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/stream.py +12 -1
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/subclient.py +0 -1
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/unit.py +68 -10
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/debuglog.py +18 -2
- ezmsg-3.4.0/src/ezmsg/util/gen_to_unit.py +195 -0
- ezmsg-3.4.0/src/ezmsg/util/generator.py +122 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/messagecodec.py +36 -22
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/messagegate.py +23 -2
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/messagelogger.py +44 -6
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/messagequeue.py +14 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/messagereplay.py +100 -27
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/messages/axisarray.py +67 -1
- ezmsg-3.4.0/src/ezmsg/util/messages/modify.py +38 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/terminate.py +30 -2
- ezmsg-3.3.3/PKG-INFO +0 -67
- ezmsg-3.3.3/README.md +0 -44
- ezmsg-3.3.3/docs/source/conf.py +0 -35
- ezmsg-3.3.3/examples/ezmsg_attach.py +0 -8
- ezmsg-3.3.3/examples/ezmsg_configs.py +0 -245
- ezmsg-3.3.3/examples/ezmsg_count.py +0 -64
- ezmsg-3.3.3/examples/ezmsg_intro.py +0 -187
- ezmsg-3.3.3/examples/ezmsg_normalterm.py +0 -123
- ezmsg-3.3.3/examples/ezmsg_stop.py +0 -117
- ezmsg-3.3.3/examples/ezmsg_toy.py +0 -198
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/__init__.py +0 -1
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/__version__.py +0 -1
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/butterworthfilter.py +0 -66
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/decimate.py +0 -40
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/downsample.py +0 -63
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/ewmfilter.py +0 -127
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/filter.py +0 -140
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/messages.py +0 -31
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/sampler.py +0 -287
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/spectral.py +0 -132
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/synth.py +0 -411
- ezmsg-3.3.3/extensions/ezmsg-sigproc/ezmsg/sigproc/window.py +0 -144
- ezmsg-3.3.3/extensions/ezmsg-sigproc/setup.py +0 -7
- ezmsg-3.3.3/extensions/ezmsg-sigproc/tests/test_butterworth.py +0 -142
- ezmsg-3.3.3/extensions/ezmsg-sigproc/tests/test_downsample.py +0 -132
- ezmsg-3.3.3/extensions/ezmsg-sigproc/tests/test_window.py +0 -139
- ezmsg-3.3.3/extensions/ezmsg-sigproc/tests/util.py +0 -25
- ezmsg-3.3.3/extensions/ezmsg-websocket/examples/ezmsg_websocket.py +0 -107
- ezmsg-3.3.3/extensions/ezmsg-websocket/ezmsg/websocket/__init__.py +0 -1
- ezmsg-3.3.3/extensions/ezmsg-websocket/ezmsg/websocket/__version__.py +0 -1
- ezmsg-3.3.3/extensions/ezmsg-websocket/ezmsg/websocket/units.py +0 -158
- ezmsg-3.3.3/extensions/ezmsg-websocket/setup.py +0 -7
- ezmsg-3.3.3/extensions/ezmsg-zmq/ezmsg/zmq/__init__.py +0 -1
- ezmsg-3.3.3/extensions/ezmsg-zmq/ezmsg/zmq/__version__.py +0 -1
- ezmsg-3.3.3/extensions/ezmsg-zmq/ezmsg/zmq/units.py +0 -218
- ezmsg-3.3.3/extensions/ezmsg-zmq/setup.py +0 -7
- ezmsg-3.3.3/ezmsg/core/settings.py +0 -31
- ezmsg-3.3.3/ezmsg/core/state.py +0 -29
- ezmsg-3.3.3/ezmsg/testing/__init__.py +0 -28
- ezmsg-3.3.3/ezmsg/testing/debuglog.py +0 -1
- ezmsg-3.3.3/ezmsg/testing/lfo.py +0 -28
- ezmsg-3.3.3/ezmsg/testing/multiplier.py +0 -44
- ezmsg-3.3.3/ezmsg/testing/terminate.py +0 -4
- ezmsg-3.3.3/ezmsg/version/__init__.py +0 -1
- ezmsg-3.3.3/ezmsg/version/__version__.py +0 -1
- ezmsg-3.3.3/ezmsg.egg-info/PKG-INFO +0 -67
- ezmsg-3.3.3/ezmsg.egg-info/SOURCES.txt +0 -99
- ezmsg-3.3.3/ezmsg.egg-info/dependency_links.txt +0 -1
- ezmsg-3.3.3/ezmsg.egg-info/entry_points.txt +0 -2
- ezmsg-3.3.3/ezmsg.egg-info/requires.txt +0 -12
- ezmsg-3.3.3/ezmsg.egg-info/top_level.txt +0 -6
- ezmsg-3.3.3/pyproject.toml +0 -6
- ezmsg-3.3.3/setup.cfg +0 -38
- ezmsg-3.3.3/setup.py +0 -3
- ezmsg-3.3.3/tests/test_addressable.py +0 -31
- ezmsg-3.3.3/tests/test_attach.py +0 -131
- ezmsg-3.3.3/tests/test_axisarray.py +0 -127
- ezmsg-3.3.3/tests/test_connections.py +0 -44
- ezmsg-3.3.3/tests/test_dag.py +0 -113
- ezmsg-3.3.3/tests/test_graph.py +0 -248
- ezmsg-3.3.3/tests/test_perf.py +0 -225
- ezmsg-3.3.3/tests/test_run.py +0 -152
- ezmsg-3.3.3/tests/test_shm.py +0 -107
- ezmsg-3.3.3/tests/test_state.py +0 -76
- ezmsg-3.3.3/tests/test_test.py +0 -8
- {ezmsg-3.3.3 → ezmsg-3.4.0}/LICENSE +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/__main__.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/addressable.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/backpressure.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/dag.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/messagemarshal.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/netprotocol.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/pubclient.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/core/util.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/__init__.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/messages/__init__.py +0 -0
- {ezmsg-3.3.3 → ezmsg-3.4.0/src}/ezmsg/util/rate.py +0 -0
ezmsg-3.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ezmsg
|
|
3
|
+
Version: 3.4.0
|
|
4
|
+
Summary: A simple DAG-based computation model
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Milsap, Griffin
|
|
7
|
+
Author-email: griffin.milsap@gmail.com
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Provides-Extra: all-ext
|
|
18
|
+
Provides-Extra: sigproc
|
|
19
|
+
Provides-Extra: vispy
|
|
20
|
+
Provides-Extra: websocket
|
|
21
|
+
Provides-Extra: zmq
|
|
22
|
+
Requires-Dist: ezmsg-sigproc ; extra == "sigproc" or extra == "all-ext"
|
|
23
|
+
Requires-Dist: ezmsg-vispy ; extra == "vispy" or extra == "all-ext"
|
|
24
|
+
Requires-Dist: ezmsg-websocket ; extra == "websocket" or extra == "all-ext"
|
|
25
|
+
Requires-Dist: ezmsg-zmq ; extra == "zmq" or extra == "all-ext"
|
|
26
|
+
Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# <img src="https://raw.githubusercontent.com/iscoe/ezmsg/main/docs/source/logo.png" width="50"/> ezmsg
|
|
30
|
+
|
|
31
|
+
Messaging and Multiprocessing.
|
|
32
|
+
|
|
33
|
+
`ezmsg` is a pure-Python implementation of a directed acyclic graph (DAG) pub/sub messaging pattern based on [`labgraph`](https://github.com/facebookresearch/labgraph) which is optimized and intended for use in constructing real-time software. `ezmsg` implements much of the `labgraph` API (with a few notable differences), and owes a lot of its design to the `labgraph` developers/project.
|
|
34
|
+
|
|
35
|
+
`ezmsg` is very fast and uses Python's `multiprocessing.shared_memory` module to facilitate efficient message passing without C++ or any compilation/build tooling.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
`pip install ezmsg`
|
|
40
|
+
|
|
41
|
+
## Dependencies
|
|
42
|
+
|
|
43
|
+
- Due to reliance on `multiprocessing.shared_memory`, `ezmsg` requires **minimum Python 3.8**.
|
|
44
|
+
- `typing_extensions`
|
|
45
|
+
|
|
46
|
+
Testing `ezmsg` requires:
|
|
47
|
+
|
|
48
|
+
- `pytest`
|
|
49
|
+
- `pytest-cov`
|
|
50
|
+
- `pytest-asyncio`
|
|
51
|
+
- `numpy`
|
|
52
|
+
|
|
53
|
+
## Setup (Development)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
$ python3 -m venv env
|
|
57
|
+
$ source env/bin/activate
|
|
58
|
+
(env) $ pip install --upgrade pip poetry
|
|
59
|
+
(env) $ poetry install --with test
|
|
60
|
+
|
|
61
|
+
(env) $ python -m pytest tests # Optionally, Perform tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Note that it is generally recommended to install poetry into its own standalone venv via the `pipx` cli tool.
|
|
65
|
+
|
|
66
|
+
## Documentation
|
|
67
|
+
|
|
68
|
+
https://ezmsg.readthedocs.io/en/latest/
|
|
69
|
+
|
|
70
|
+
`ezmsg` is very similar to [`labgraph`](https://www.github.com/facebookresearch/labgraph), so you might get a primer with their documentation and examples. Additionally, there are many examples provided in the examples/tests directories strewn throughout this repository.
|
|
71
|
+
|
|
72
|
+
## Extensions
|
|
73
|
+
|
|
74
|
+
`ezmsg` extensions can be installed individually or all at once. To install all the extension packages in one go, you can use the following command:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install "ezmsg[all_ext]"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This will install all the available public extension packages for `ezmsg` that are listed in `pyproject.toml`.
|
|
81
|
+
If you prefer to install a subset of extension packages, you can use the following command:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install "ezmsg[zmq,sigproc,...]"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Please note that the `ezmsg` package itself can still be installed without any additional extensions using `pip install ezmsg`.
|
|
88
|
+
|
|
89
|
+
Extensions can be managed manually as well. Here are some of the extensions we manage or are aware of:
|
|
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
|
|
94
|
+
- [ezmsg-panel](https://github.com/griffinmilsap/ezmsg-panel) -- Plotting tools for `ezmsg` that use [panel](https://github.com/holoviz/panel)
|
|
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
|
|
97
|
+
- [ezmsg-unicorn](https://github.com/griffinmilsap/ezmsg-unicorn) -- g.tec Unicorn Hybrid Black integration for `ezmsg`
|
|
98
|
+
- [ezmsg-gadget](https://github.com/griffinmilsap/ezmsg-gadget) -- USB-gadget with HID control integration for Raspberry Pi (Zero/W/2W, 4, CM4)
|
|
99
|
+
- [ezmsg-openbci](https://github.com/griffinmilsap/ezmsg-openbci) -- OpenBCI Cyton serial interface for `ezmsg`
|
|
100
|
+
- [ezmsg-ssvep](https://github.com/griffinmilsap/ezmsg-ssvep) -- Tools for running SSVEP experiments with `ezmsg`
|
|
101
|
+
- [ezmsg-vispy](https://github.com/pperanich/ezmsg-vispy) -- `ezmsg` visualization toolkit using PyQt6 and vispy.
|
|
102
|
+
|
|
103
|
+
## Publications
|
|
104
|
+
|
|
105
|
+
A collection of academic papers, journals, and other publications that have cited or utilized `ezmsg` in research and development.
|
|
106
|
+
These publications provide insights into the practical applications and impact of `ezmsg` in various fields.
|
|
107
|
+
|
|
108
|
+
- [A click-based electrocorticographic brain-computer interface enables long-term high-performance switch-scan spelling](https://doi.org/10.21203/rs.3.rs-3158792/v1)
|
|
109
|
+
- [Stable Decoding from a Speech BCI Enables Control for an Individual with ALS without Recalibration for 3 Months](https://doi.org/10.1002/advs.202304853)
|
|
110
|
+
|
|
111
|
+
## Financial Support
|
|
112
|
+
|
|
113
|
+
`ezmsg` is supported by Johns Hopkins University (JHU), the JHU Applied Physics Laboratory (APL), and by the Wyss Center for Bio and Neuro Engineering.
|
|
114
|
+
|
ezmsg-3.4.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# <img src="https://raw.githubusercontent.com/iscoe/ezmsg/main/docs/source/logo.png" width="50"/> ezmsg
|
|
2
|
+
|
|
3
|
+
Messaging and Multiprocessing.
|
|
4
|
+
|
|
5
|
+
`ezmsg` is a pure-Python implementation of a directed acyclic graph (DAG) pub/sub messaging pattern based on [`labgraph`](https://github.com/facebookresearch/labgraph) which is optimized and intended for use in constructing real-time software. `ezmsg` implements much of the `labgraph` API (with a few notable differences), and owes a lot of its design to the `labgraph` developers/project.
|
|
6
|
+
|
|
7
|
+
`ezmsg` is very fast and uses Python's `multiprocessing.shared_memory` module to facilitate efficient message passing without C++ or any compilation/build tooling.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
`pip install ezmsg`
|
|
12
|
+
|
|
13
|
+
## Dependencies
|
|
14
|
+
|
|
15
|
+
- Due to reliance on `multiprocessing.shared_memory`, `ezmsg` requires **minimum Python 3.8**.
|
|
16
|
+
- `typing_extensions`
|
|
17
|
+
|
|
18
|
+
Testing `ezmsg` requires:
|
|
19
|
+
|
|
20
|
+
- `pytest`
|
|
21
|
+
- `pytest-cov`
|
|
22
|
+
- `pytest-asyncio`
|
|
23
|
+
- `numpy`
|
|
24
|
+
|
|
25
|
+
## Setup (Development)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
$ python3 -m venv env
|
|
29
|
+
$ source env/bin/activate
|
|
30
|
+
(env) $ pip install --upgrade pip poetry
|
|
31
|
+
(env) $ poetry install --with test
|
|
32
|
+
|
|
33
|
+
(env) $ python -m pytest tests # Optionally, Perform tests
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Note that it is generally recommended to install poetry into its own standalone venv via the `pipx` cli tool.
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
https://ezmsg.readthedocs.io/en/latest/
|
|
41
|
+
|
|
42
|
+
`ezmsg` is very similar to [`labgraph`](https://www.github.com/facebookresearch/labgraph), so you might get a primer with their documentation and examples. Additionally, there are many examples provided in the examples/tests directories strewn throughout this repository.
|
|
43
|
+
|
|
44
|
+
## Extensions
|
|
45
|
+
|
|
46
|
+
`ezmsg` extensions can be installed individually or all at once. To install all the extension packages in one go, you can use the following command:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install "ezmsg[all_ext]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This will install all the available public extension packages for `ezmsg` that are listed in `pyproject.toml`.
|
|
53
|
+
If you prefer to install a subset of extension packages, you can use the following command:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install "ezmsg[zmq,sigproc,...]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Please note that the `ezmsg` package itself can still be installed without any additional extensions using `pip install ezmsg`.
|
|
60
|
+
|
|
61
|
+
Extensions can be managed manually as well. Here are some of the extensions we manage or are aware of:
|
|
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
|
|
66
|
+
- [ezmsg-panel](https://github.com/griffinmilsap/ezmsg-panel) -- Plotting tools for `ezmsg` that use [panel](https://github.com/holoviz/panel)
|
|
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
|
|
69
|
+
- [ezmsg-unicorn](https://github.com/griffinmilsap/ezmsg-unicorn) -- g.tec Unicorn Hybrid Black integration for `ezmsg`
|
|
70
|
+
- [ezmsg-gadget](https://github.com/griffinmilsap/ezmsg-gadget) -- USB-gadget with HID control integration for Raspberry Pi (Zero/W/2W, 4, CM4)
|
|
71
|
+
- [ezmsg-openbci](https://github.com/griffinmilsap/ezmsg-openbci) -- OpenBCI Cyton serial interface for `ezmsg`
|
|
72
|
+
- [ezmsg-ssvep](https://github.com/griffinmilsap/ezmsg-ssvep) -- Tools for running SSVEP experiments with `ezmsg`
|
|
73
|
+
- [ezmsg-vispy](https://github.com/pperanich/ezmsg-vispy) -- `ezmsg` visualization toolkit using PyQt6 and vispy.
|
|
74
|
+
|
|
75
|
+
## Publications
|
|
76
|
+
|
|
77
|
+
A collection of academic papers, journals, and other publications that have cited or utilized `ezmsg` in research and development.
|
|
78
|
+
These publications provide insights into the practical applications and impact of `ezmsg` in various fields.
|
|
79
|
+
|
|
80
|
+
- [A click-based electrocorticographic brain-computer interface enables long-term high-performance switch-scan spelling](https://doi.org/10.21203/rs.3.rs-3158792/v1)
|
|
81
|
+
- [Stable Decoding from a Speech BCI Enables Control for an Individual with ALS without Recalibration for 3 Months](https://doi.org/10.1002/advs.202304853)
|
|
82
|
+
|
|
83
|
+
## Financial Support
|
|
84
|
+
|
|
85
|
+
`ezmsg` is supported by Johns Hopkins University (JHU), the JHU Applied Physics Laboratory (APL), and by the Wyss Center for Bio and Neuro Engineering.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "ezmsg"
|
|
3
|
+
version = "3.4.0"
|
|
4
|
+
description = "A simple DAG-based computation model"
|
|
5
|
+
authors = [
|
|
6
|
+
"Milsap, Griffin <griffin.milsap@gmail.com>",
|
|
7
|
+
"Peranich, Preston <pperanich@gmail.com>",
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
packages = [{ include = "ezmsg", from = "src" }]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.poetry.scripts]
|
|
18
|
+
ezmsg = "ezmsg.core.command:cmdline"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.dependencies]
|
|
21
|
+
python = "^3.8"
|
|
22
|
+
typing-extensions = "^4.9.0"
|
|
23
|
+
ezmsg-sigproc = { version = "*", source = "pypi", optional = true }
|
|
24
|
+
ezmsg-websocket = { version = "*", source = "pypi", optional = true }
|
|
25
|
+
ezmsg-zmq = { version = "*", source = "pypi", optional = true }
|
|
26
|
+
ezmsg-vispy = { version = "*", source = "pypi", optional = true }
|
|
27
|
+
# ezmsg-panel = { git = "https://github.com/griffinmilsap/ezmsg-panel.git", rev = "main", optional = true }
|
|
28
|
+
# ezmsg-blackrock = { python = "^3.9", git = "https://github.com/CerebusOSS/ezmsg-blackrock.git", rev = "main", optional = true }
|
|
29
|
+
# ezmsg-unicorn = { git = "https://github.com/griffinmilsap/ezmsg-unicorn.git", rev = "main", optional = true }
|
|
30
|
+
# ezmsg-gadget = { git = "https://github.com/griffinmilsap/ezmsg-gadget.git", rev = "main", optional = true }
|
|
31
|
+
# ezmsg-openbci = { git = "https://github.com/griffinmilsap/ezmsg-openbci.git", rev = "main", optional = true }
|
|
32
|
+
# ezmsg-ssvep = { git = "https://github.com/griffinmilsap/ezmsg-ssvep.git", rev = "main", optional = true }
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[tool.poetry.group.test.dependencies]
|
|
36
|
+
pytest = "^7.0.0"
|
|
37
|
+
pytest-asyncio = "*"
|
|
38
|
+
pytest-cov = "*"
|
|
39
|
+
numpy = "^1.24.2"
|
|
40
|
+
flake8 = "*"
|
|
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
|
+
|
|
48
|
+
[tool.poetry.extras]
|
|
49
|
+
sigproc = ["ezmsg-sigproc"]
|
|
50
|
+
websocket = ["ezmsg-websocket"]
|
|
51
|
+
zmq = ["ezmsg-zmq"]
|
|
52
|
+
# panel = ["ezmsg-panel"]
|
|
53
|
+
# blackrock = ["ezmsg-blackrock"]
|
|
54
|
+
# unicorn = ["ezmsg-unicorn"]
|
|
55
|
+
# gadget = ["ezmsg-gadget"]
|
|
56
|
+
# openbci = ["ezmsg-openbci"]
|
|
57
|
+
# ssvep = ["ezmsg-ssvep"]
|
|
58
|
+
vispy = ["ezmsg-vispy"]
|
|
59
|
+
all_ext = [
|
|
60
|
+
"ezmsg-sigproc",
|
|
61
|
+
"ezmsg-websocket",
|
|
62
|
+
"ezmsg-zmq",
|
|
63
|
+
# "ezmsg-panel",
|
|
64
|
+
# "ezmsg-blackrock",
|
|
65
|
+
# "ezmsg-unicorn",
|
|
66
|
+
# "ezmsg-gadget",
|
|
67
|
+
# "ezmsg-openbci",
|
|
68
|
+
# "ezmsg-ssvep",
|
|
69
|
+
"ezmsg-vispy",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
addopts = ["--import-mode=importlib"]
|
|
74
|
+
pythonpath = ["src", "tests"]
|
|
75
|
+
testpaths = "tests"
|
|
76
|
+
|
|
77
|
+
[build-system]
|
|
78
|
+
requires = ["poetry-core"]
|
|
79
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__version__ = importlib.metadata.version("ezmsg")
|
|
5
|
+
|
|
1
6
|
__all__ = [
|
|
2
7
|
"task",
|
|
3
8
|
"publisher",
|
|
@@ -60,3 +65,4 @@ logger.addHandler(handler)
|
|
|
60
65
|
|
|
61
66
|
LOGLEVEL = os.environ.get("EZMSG_LOGLEVEL", "INFO").upper()
|
|
62
67
|
logger.setLevel(LOGLEVEL)
|
|
68
|
+
logger.debug(f'Log level set to {LOGLEVEL}')
|
|
@@ -167,6 +167,29 @@ 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
|
+
"""
|
|
191
|
+
# FIXME: This function is the last major re-implementation needed to make this
|
|
192
|
+
# codebase more maintainable.
|
|
170
193
|
graph_service = GraphService(graph_address)
|
|
171
194
|
shm_service = SHMService()
|
|
172
195
|
|
|
@@ -195,16 +218,20 @@ def run(
|
|
|
195
218
|
if execution_context is None:
|
|
196
219
|
return
|
|
197
220
|
|
|
221
|
+
# FIXME: When done this way, we don't exit the graph_context on exception
|
|
198
222
|
async def create_graph_context() -> GraphContext:
|
|
199
223
|
return await GraphContext(graph_service, shm_service).__aenter__()
|
|
200
224
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
225
|
+
# FIXME: This sort of stuff should all be done in a separate async function...
|
|
226
|
+
# Done this way, its ugly as hell and opens us up to a lot of issues with
|
|
227
|
+
# entering and exiting context properly on exceptions.
|
|
204
228
|
graph_context = asyncio.run_coroutine_threadsafe(
|
|
205
229
|
create_graph_context(), loop
|
|
206
230
|
).result()
|
|
207
231
|
|
|
232
|
+
async def cleanup_graph() -> None:
|
|
233
|
+
await graph_context.__aexit__(None, None, None)
|
|
234
|
+
|
|
208
235
|
async def setup_graph() -> None:
|
|
209
236
|
for edge in execution_context.connections:
|
|
210
237
|
await graph_context.connect(*edge)
|
|
@@ -252,9 +279,7 @@ def run(
|
|
|
252
279
|
|
|
253
280
|
finally:
|
|
254
281
|
join_all_other_processes()
|
|
255
|
-
asyncio.run_coroutine_threadsafe(
|
|
256
|
-
cleanup_graph(graph_context), loop
|
|
257
|
-
).result()
|
|
282
|
+
asyncio.run_coroutine_threadsafe(cleanup_graph(), loop).result()
|
|
258
283
|
|
|
259
284
|
|
|
260
285
|
def collect_processes(
|
|
@@ -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")
|
|
@@ -386,3 +389,4 @@ def new_threaded_event_loop(
|
|
|
386
389
|
loop.call_soon_threadsafe(loop.stop)
|
|
387
390
|
thread.join()
|
|
388
391
|
loop.close()
|
|
392
|
+
logger.debug("Task thread stopped and closed")
|
|
@@ -34,21 +34,36 @@ 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
|
-
def __init__(self, settings: typing.Optional[Settings] = None):
|
|
40
|
-
super(Collection, self).__init__(settings)
|
|
41
|
+
def __init__(self, *args, settings: typing.Optional[Settings] = None, **kwargs):
|
|
42
|
+
super(Collection, self).__init__(*args, settings=settings, **kwargs)
|
|
41
43
|
|
|
42
44
|
self._components = deepcopy(self.__class__.__components__)
|
|
43
45
|
for comp_name, comp in self.components.items():
|
|
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,9 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
3
|
+
import base64
|
|
2
4
|
import asyncio
|
|
3
5
|
import argparse
|
|
4
6
|
import logging
|
|
5
7
|
import subprocess
|
|
6
8
|
import typing
|
|
9
|
+
import webbrowser
|
|
7
10
|
|
|
8
11
|
from collections import defaultdict
|
|
9
12
|
from uuid import uuid4
|
|
@@ -43,7 +46,7 @@ def cmdline() -> None:
|
|
|
43
46
|
parser.add_argument(
|
|
44
47
|
"command",
|
|
45
48
|
help="command for ezmsg",
|
|
46
|
-
choices=["serve", "start", "shutdown", "graphviz"],
|
|
49
|
+
choices=["serve", "start", "shutdown", "graphviz", "mermaid"],
|
|
47
50
|
)
|
|
48
51
|
|
|
49
52
|
parser.add_argument("--address", help="Address for GraphServer", default=None)
|
|
@@ -95,7 +98,7 @@ async def run_command(cmd: str, graph_address: Address, shm_address: Address) ->
|
|
|
95
98
|
|
|
96
99
|
elif cmd == "start":
|
|
97
100
|
popen = subprocess.Popen(
|
|
98
|
-
[
|
|
101
|
+
[sys.executable, "-m", "ezmsg.core", "serve", f"--address={graph_address}"]
|
|
99
102
|
)
|
|
100
103
|
|
|
101
104
|
while True:
|
|
@@ -113,10 +116,14 @@ async def run_command(cmd: str, graph_address: Address, shm_address: Address) ->
|
|
|
113
116
|
elif cmd == "shutdown":
|
|
114
117
|
try:
|
|
115
118
|
await graph_service.shutdown()
|
|
116
|
-
logger.info(
|
|
119
|
+
logger.info(
|
|
120
|
+
f"Issued shutdown command to GraphServer @ {graph_service.address}"
|
|
121
|
+
)
|
|
117
122
|
|
|
118
123
|
except ConnectionRefusedError:
|
|
119
|
-
logger.warning(
|
|
124
|
+
logger.warning(
|
|
125
|
+
f"Could not issue shutdown command to GraphServer @ {graph_service.address}; server not running?"
|
|
126
|
+
)
|
|
120
127
|
|
|
121
128
|
elif cmd == "graphviz":
|
|
122
129
|
try:
|
|
@@ -148,7 +155,9 @@ async def run_command(cmd: str, graph_address: Address, shm_address: Address) ->
|
|
|
148
155
|
new_conns = conns.copy()
|
|
149
156
|
new_conns.remove(proxy_topic)
|
|
150
157
|
new_conns.union(downstreams)
|
|
151
|
-
logger.info(
|
|
158
|
+
logger.info(
|
|
159
|
+
f"Updating connections for {node} from {conns} to {new_conns}"
|
|
160
|
+
)
|
|
152
161
|
graph_connections[node] = new_conns
|
|
153
162
|
|
|
154
163
|
graph_connections = dag.graph.copy()
|
|
@@ -192,7 +201,7 @@ async def run_command(cmd: str, graph_address: Address, shm_address: Address) ->
|
|
|
192
201
|
),
|
|
193
202
|
IND,
|
|
194
203
|
)
|
|
195
|
-
elif
|
|
204
|
+
elif isinstance(g[leaf], str):
|
|
196
205
|
out += indent(f"{node_map[g[leaf]]} [label={leaf}];" + "\n", IND)
|
|
197
206
|
return out
|
|
198
207
|
|
|
@@ -209,3 +218,102 @@ async def run_command(cmd: str, graph_address: Address, shm_address: Address) ->
|
|
|
209
218
|
)
|
|
210
219
|
|
|
211
220
|
print(graphviz_out)
|
|
221
|
+
elif cmd == "mermaid":
|
|
222
|
+
try:
|
|
223
|
+
dag: DAG = await graph_service.dag()
|
|
224
|
+
except (ConnectionRefusedError, ConnectionResetError):
|
|
225
|
+
logger.info(
|
|
226
|
+
f"GraphServer not running @{graph_address}, or host is refusing connections"
|
|
227
|
+
)
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
graph_connections = dag.graph.copy()
|
|
231
|
+
# Let's eliminate proxy topics, i.e. connections with inputs and outputs.
|
|
232
|
+
source_nodes = []
|
|
233
|
+
for node, conns in graph_connections.items():
|
|
234
|
+
if len(conns) > 0:
|
|
235
|
+
source_nodes += [node]
|
|
236
|
+
proxy_topics = []
|
|
237
|
+
for conns in graph_connections.values():
|
|
238
|
+
for conn in conns:
|
|
239
|
+
if conn in source_nodes and conn not in proxy_topics:
|
|
240
|
+
proxy_topics += [conn]
|
|
241
|
+
# Replace Proxy Topics with actual source and downstream
|
|
242
|
+
for proxy_topic in proxy_topics:
|
|
243
|
+
downstreams = graph_connections.pop(proxy_topic)
|
|
244
|
+
logger.info(f"{proxy_topic} downstream connetions: {downstreams}")
|
|
245
|
+
for node, conns in graph_connections.items():
|
|
246
|
+
for conn in conns:
|
|
247
|
+
if conn == proxy_topic:
|
|
248
|
+
new_conns = conns.copy()
|
|
249
|
+
new_conns.remove(proxy_topic)
|
|
250
|
+
new_conns.union(downstreams)
|
|
251
|
+
logger.info(
|
|
252
|
+
f"Updating connections for {node} from {conns} to {new_conns}"
|
|
253
|
+
)
|
|
254
|
+
graph_connections[node] = new_conns
|
|
255
|
+
|
|
256
|
+
graph_connections = dag.graph.copy()
|
|
257
|
+
# Let's come up with UUID node names
|
|
258
|
+
nodes = set(graph_connections.keys())
|
|
259
|
+
node_map = {name: f"{str(uuid4())}" for name in nodes}
|
|
260
|
+
|
|
261
|
+
# Construct the graph
|
|
262
|
+
def tree():
|
|
263
|
+
return defaultdict(tree)
|
|
264
|
+
|
|
265
|
+
graph: defaultdict = tree()
|
|
266
|
+
|
|
267
|
+
connections = ""
|
|
268
|
+
for node, conns in graph_connections.items():
|
|
269
|
+
subgraph = graph
|
|
270
|
+
path = node.split("/")
|
|
271
|
+
route = path[:-1]
|
|
272
|
+
stream = path[-1]
|
|
273
|
+
for seg in route:
|
|
274
|
+
subgraph = subgraph[seg]
|
|
275
|
+
subgraph[stream] = node
|
|
276
|
+
|
|
277
|
+
for sub in conns:
|
|
278
|
+
connections += f"{node_map[node]} --> {node_map[sub]}" + "\n"
|
|
279
|
+
|
|
280
|
+
# Now convert to dot syntax
|
|
281
|
+
def recurse_graph(g: defaultdict):
|
|
282
|
+
out = ""
|
|
283
|
+
for leaf in g:
|
|
284
|
+
if type(g[leaf]) == defaultdict:
|
|
285
|
+
out += indent(
|
|
286
|
+
"\n".join(
|
|
287
|
+
[
|
|
288
|
+
f"subgraph {leaf.lower()} [{leaf}]",
|
|
289
|
+
# "direction LR",
|
|
290
|
+
f"{recurse_graph(g[leaf])}",
|
|
291
|
+
"end",
|
|
292
|
+
"",
|
|
293
|
+
]
|
|
294
|
+
),
|
|
295
|
+
IND,
|
|
296
|
+
)
|
|
297
|
+
elif isinstance(g[leaf], str):
|
|
298
|
+
out += indent(f"{node_map[g[leaf]]}[{leaf}]" + "\n", IND)
|
|
299
|
+
return out
|
|
300
|
+
|
|
301
|
+
subgraph_tree = recurse_graph(graph)
|
|
302
|
+
|
|
303
|
+
mermaid = "\n".join(
|
|
304
|
+
[
|
|
305
|
+
"flowchart LR",
|
|
306
|
+
subgraph_tree,
|
|
307
|
+
indent(connections, IND),
|
|
308
|
+
]
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# print(mermaid)
|
|
312
|
+
webbrowser.open(mm(mermaid))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def mm(graph):
|
|
316
|
+
graphbytes = graph.encode("utf8")
|
|
317
|
+
base64_bytes = base64.b64encode(graphbytes)
|
|
318
|
+
base64_string = base64_bytes.decode("ascii")
|
|
319
|
+
return f"https://mermaid.ink/img/{base64_string}"
|