ezmsg 3.6.0__tar.gz → 3.6.2__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.6.0 → ezmsg-3.6.2}/PKG-INFO +6 -3
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/other.rst +7 -1
- {ezmsg-3.6.0 → ezmsg-3.6.2}/pyproject.toml +8 -3
- ezmsg-3.6.2/src/ezmsg/core/command.py +178 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/graphserver.py +133 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/netprotocol.py +1 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messagelogger.py +46 -16
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messages/axisarray.py +13 -6
- {ezmsg-3.6.0 → ezmsg-3.6.2}/uv.lock +5 -1
- ezmsg-3.6.0/.envrc +0 -2
- ezmsg-3.6.0/src/ezmsg/core/command.py +0 -319
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.flake8 +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.git-blame-ignore-revs +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.github/workflows/ci.yml +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.github/workflows/publish.yml +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.gitignore +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.pre-commit-config.yaml +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.python-version +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/.readthedocs.yaml +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/LICENSE +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/README.md +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/Makefile +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/make.bat +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/requirements.txt +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/about.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/api.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/conf.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/developer.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/extensions/sigproc.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/extensions.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/getting-started.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/index.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/logo.png +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/docs/source/utils.rst +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/.gitignore +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_attach.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_configs.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_count.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_generator.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_intro.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_log_to_file.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_normalterm.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_stop.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/examples/ezmsg_toy.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/__init__.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/__main__.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/addressable.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/backend.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/backendprocess.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/backpressure.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/collection.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/component.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/dag.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/graphcontext.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/message.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/messagecache.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/messagemarshal.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/pubclient.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/server.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/settings.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/shmserver.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/state.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/stream.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/subclient.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/unit.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/core/util.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/__init__.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/debuglog.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/gen_to_unit.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/generator.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messagecodec.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messagegate.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messagequeue.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messagereplay.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messages/__init__.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messages/chunker.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messages/key.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messages/modify.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/messages/util.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/perf_test.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/rate.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/src/ezmsg/util/terminate.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/__init__.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/ez_test_utils.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/messages/__init__.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/messages/test_axisarray.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/messages/test_chunker.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/messages/test_key.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/messages/test_modify.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_addressable.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_attach.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_connections.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_dag.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_generator.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_graph.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_run.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_shm.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_state.py +0 -0
- {ezmsg-3.6.0 → ezmsg-3.6.2}/tests/test_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg
|
|
3
|
-
Version: 3.6.
|
|
3
|
+
Version: 3.6.2
|
|
4
4
|
Summary: A simple DAG-based computation model
|
|
5
5
|
Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -12,17 +12,20 @@ License: MIT License
|
|
|
12
12
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
13
13
|
|
|
14
14
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
15
|
+
License-File: LICENSE
|
|
15
16
|
Classifier: Operating System :: OS Independent
|
|
16
17
|
Classifier: Programming Language :: Python :: 3
|
|
17
18
|
Requires-Python: >=3.8
|
|
18
19
|
Requires-Dist: typing-extensions>=4.9.0
|
|
20
|
+
Provides-Extra: axisarray
|
|
21
|
+
Requires-Dist: numpy>=1.24.4; extra == 'axisarray'
|
|
19
22
|
Provides-Extra: docs
|
|
20
23
|
Requires-Dist: ezmsg-sigproc>=1.2.3; extra == 'docs'
|
|
21
24
|
Requires-Dist: sphinx-rtd-theme==2.0.0; extra == 'docs'
|
|
22
25
|
Requires-Dist: sphinx<=7.2; extra == 'docs'
|
|
23
26
|
Provides-Extra: test
|
|
24
27
|
Requires-Dist: flake8>=5.0.4; extra == 'test'
|
|
25
|
-
Requires-Dist: numpy>=1.24.
|
|
28
|
+
Requires-Dist: numpy>=1.24.4; extra == 'test'
|
|
26
29
|
Requires-Dist: pytest-asyncio>=0.23.8; extra == 'test'
|
|
27
30
|
Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
|
|
28
31
|
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
@@ -14,4 +14,10 @@ Still under construction!
|
|
|
14
14
|
Notebooks
|
|
15
15
|
---------
|
|
16
16
|
|
|
17
|
-
An ezmsg pipeline can be run in a notebook environment with an extra parameter. When using ``run()``, include the optional kwarg ``force_single_process=True``.
|
|
17
|
+
An ezmsg pipeline can be run in a notebook environment with an extra parameter. When using ``run()``, include the optional kwarg ``force_single_process=True``.
|
|
18
|
+
|
|
19
|
+
Media
|
|
20
|
+
-----
|
|
21
|
+
|
|
22
|
+
* `BCI Society 2025 Poster <https://github.com/ezmsg-org/ezmsg/releases/download/V3.6.1/ezmsg_poster_BCI2025.pdf>`_
|
|
23
|
+
* `Society For Neuroscience 2024 Poster <https://github.com/ezmsg-org/ezmsg/releases/download/V3.6.1/ezmsg_poster_SfN2024.pdf>`_
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ezmsg"
|
|
3
|
-
version = "3.6.
|
|
3
|
+
version = "3.6.2"
|
|
4
4
|
description = "A simple DAG-based computation model"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Griffin Milsap", email = "griffin.milsap@gmail.com" },
|
|
@@ -14,18 +14,23 @@ classifiers = [
|
|
|
14
14
|
"Operating System :: OS Independent",
|
|
15
15
|
]
|
|
16
16
|
requires-python = ">=3.8"
|
|
17
|
-
dependencies = [
|
|
17
|
+
dependencies = [
|
|
18
|
+
"typing-extensions >= 4.9.0",
|
|
19
|
+
]
|
|
18
20
|
|
|
19
21
|
[project.optional-dependencies]
|
|
20
22
|
test = [
|
|
21
23
|
"pytest>=7.0.0",
|
|
22
|
-
"numpy>=1.24.2",
|
|
23
24
|
"pytest-asyncio>=0.23.8",
|
|
24
25
|
"pytest-cov>=5.0.0",
|
|
25
26
|
"flake8>=5.0.4",
|
|
26
27
|
"xarray>=2023.1.0;python_version<'3.13'",
|
|
28
|
+
"numpy>=1.24.4",
|
|
27
29
|
]
|
|
28
30
|
docs = ["sphinx<=7.2", "sphinx-rtd-theme==2.0.0", "ezmsg-sigproc>=1.2.3"]
|
|
31
|
+
axisarray = [
|
|
32
|
+
"numpy>=1.24.4",
|
|
33
|
+
]
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
[project.scripts]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import typing
|
|
10
|
+
import webbrowser
|
|
11
|
+
import zlib
|
|
12
|
+
|
|
13
|
+
from .graphserver import GraphService
|
|
14
|
+
from .shmserver import SHMService
|
|
15
|
+
from .netprotocol import (
|
|
16
|
+
Address,
|
|
17
|
+
GRAPHSERVER_ADDR_ENV,
|
|
18
|
+
GRAPHSERVER_PORT_DEFAULT,
|
|
19
|
+
SHMSERVER_ADDR_ENV,
|
|
20
|
+
SHMSERVER_PORT_DEFAULT,
|
|
21
|
+
PUBLISHER_START_PORT_ENV,
|
|
22
|
+
PUBLISHER_START_PORT_DEFAULT,
|
|
23
|
+
close_stream_writer,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("ezmsg")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmdline() -> None:
|
|
30
|
+
parser = argparse.ArgumentParser(
|
|
31
|
+
"ezmsg.core",
|
|
32
|
+
description="start and stop core ezmsg server processes",
|
|
33
|
+
epilog=f"""
|
|
34
|
+
You can also change server configuration with environment variables.
|
|
35
|
+
GraphServer will be hosted on ${GRAPHSERVER_ADDR_ENV} (default port: {GRAPHSERVER_PORT_DEFAULT}).
|
|
36
|
+
SHMServer will be hosted on ${SHMSERVER_ADDR_ENV} (default port: {SHMSERVER_PORT_DEFAULT}).
|
|
37
|
+
Publishers will be assigned available ports starting from {PUBLISHER_START_PORT_DEFAULT}. (Change with ${PUBLISHER_START_PORT_ENV})
|
|
38
|
+
""",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"command",
|
|
43
|
+
help="command for ezmsg",
|
|
44
|
+
choices=["serve", "start", "shutdown", "graphviz", "mermaid"],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
parser.add_argument("--address", help="Address for GraphServer", default=None)
|
|
48
|
+
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--target",
|
|
51
|
+
help="Target for mermaid output. Options are 'ink', 'live', and 'play'.",
|
|
52
|
+
default="live",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
class Args:
|
|
56
|
+
command: str
|
|
57
|
+
address: typing.Optional[str]
|
|
58
|
+
target: str
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args(namespace=Args)
|
|
61
|
+
|
|
62
|
+
graph_address = Address("127.0.0.1", GRAPHSERVER_PORT_DEFAULT)
|
|
63
|
+
if args.address is not None:
|
|
64
|
+
graph_address = Address.from_string(args.address)
|
|
65
|
+
shm_address_str = os.environ.get(
|
|
66
|
+
SHMSERVER_ADDR_ENV, f"127.0.0.1:{SHMSERVER_PORT_DEFAULT}"
|
|
67
|
+
)
|
|
68
|
+
shm_address = Address.from_string(shm_address_str)
|
|
69
|
+
|
|
70
|
+
loop = asyncio.new_event_loop()
|
|
71
|
+
asyncio.set_event_loop(loop)
|
|
72
|
+
|
|
73
|
+
loop.run_until_complete(
|
|
74
|
+
run_command(args.command, graph_address, shm_address, args.target)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def run_command(
|
|
79
|
+
cmd: str, graph_address: Address, shm_address: Address, target: str = "live"
|
|
80
|
+
) -> None:
|
|
81
|
+
shm_service = SHMService(shm_address)
|
|
82
|
+
graph_service = GraphService(graph_address)
|
|
83
|
+
|
|
84
|
+
if cmd == "serve":
|
|
85
|
+
logger.info(f"GraphServer Address: {graph_address}")
|
|
86
|
+
logger.info(f"SHMServer Address: {shm_address}")
|
|
87
|
+
|
|
88
|
+
shm_server = shm_service.create_server()
|
|
89
|
+
graph_server = graph_service.create_server()
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
logger.info("Servers running...")
|
|
93
|
+
graph_server.join()
|
|
94
|
+
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
logger.info("Interrupt detected; shutting down servers")
|
|
97
|
+
|
|
98
|
+
finally:
|
|
99
|
+
if graph_server is not None:
|
|
100
|
+
graph_server.stop()
|
|
101
|
+
|
|
102
|
+
if shm_server is not None:
|
|
103
|
+
shm_server.stop()
|
|
104
|
+
|
|
105
|
+
elif cmd == "start":
|
|
106
|
+
popen = subprocess.Popen(
|
|
107
|
+
[sys.executable, "-m", "ezmsg.core", "serve", f"--address={graph_address}"]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
while True:
|
|
111
|
+
try:
|
|
112
|
+
_, writer = await graph_service.open_connection()
|
|
113
|
+
await close_stream_writer(writer)
|
|
114
|
+
_, writer = await shm_service.open_connection()
|
|
115
|
+
await close_stream_writer(writer)
|
|
116
|
+
break
|
|
117
|
+
except ConnectionRefusedError:
|
|
118
|
+
await asyncio.sleep(0.1)
|
|
119
|
+
|
|
120
|
+
logger.info(f"Forked ezmsg servers in PID: {popen.pid}")
|
|
121
|
+
|
|
122
|
+
elif cmd == "shutdown":
|
|
123
|
+
try:
|
|
124
|
+
await graph_service.shutdown()
|
|
125
|
+
logger.info(
|
|
126
|
+
f"Issued shutdown command to GraphServer @ {graph_service.address}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
except ConnectionRefusedError:
|
|
130
|
+
logger.warning(
|
|
131
|
+
f"Could not issue shutdown command to GraphServer @ {graph_service.address}; server not running?"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
elif cmd in ["graphviz", "mermaid"]:
|
|
135
|
+
graph_out = await graph_service.get_formatted_graph(cmd)
|
|
136
|
+
print(graph_out)
|
|
137
|
+
if cmd == "mermaid" and target == "live":
|
|
138
|
+
print(
|
|
139
|
+
"%% If the graph does not render immediately, try toggling the 'Pan & Zoom' button."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if cmd == "mermaid":
|
|
143
|
+
webbrowser.open(mm(graph_out, target=target))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def mm(graph: str, target="live") -> str:
|
|
147
|
+
if target != "ink":
|
|
148
|
+
jdict = {
|
|
149
|
+
"code": graph,
|
|
150
|
+
"mermaid": {"theme": "default"},
|
|
151
|
+
"updateDiagram": True,
|
|
152
|
+
"autoSync": True,
|
|
153
|
+
"rough": False,
|
|
154
|
+
}
|
|
155
|
+
graph = json.dumps(jdict)
|
|
156
|
+
graphbytes: bytes = graph.encode("utf8")
|
|
157
|
+
|
|
158
|
+
if target != "ink":
|
|
159
|
+
compress = zlib.compressobj(9, zlib.DEFLATED, 15, 8, zlib.Z_DEFAULT_STRATEGY)
|
|
160
|
+
graphbytes = compress.compress(graphbytes)
|
|
161
|
+
graphbytes += compress.flush()
|
|
162
|
+
|
|
163
|
+
base64_bytes = base64.b64encode(graphbytes)
|
|
164
|
+
base64_string = base64_bytes.decode("ascii")
|
|
165
|
+
|
|
166
|
+
if target == "ink":
|
|
167
|
+
prefix = "https://mermaid.ink/img/"
|
|
168
|
+
elif target in ["live", "play"]:
|
|
169
|
+
type_str = "pako" # or "base64" if we skip compression above.
|
|
170
|
+
if target == "live":
|
|
171
|
+
prefix = f"https://mermaid.live/edit#{type_str}:"
|
|
172
|
+
else: # "play"
|
|
173
|
+
prefix = f"https://www.mermaidchart.com/play#{type_str}:"
|
|
174
|
+
else:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Unknown mermaid target '{target}'. Available options are 'ink', 'live', or 'play'."
|
|
177
|
+
)
|
|
178
|
+
return prefix + base64_string
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
import logging
|
|
3
4
|
import pickle
|
|
5
|
+
from textwrap import indent
|
|
4
6
|
import typing
|
|
7
|
+
from uuid import uuid4
|
|
5
8
|
|
|
6
9
|
from contextlib import suppress
|
|
7
10
|
|
|
@@ -28,6 +31,8 @@ from .netprotocol import (
|
|
|
28
31
|
|
|
29
32
|
logger = logging.getLogger("ezmsg")
|
|
30
33
|
|
|
34
|
+
IND = " "
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
class GraphServer(ThreadedAsyncServer):
|
|
33
38
|
"""
|
|
@@ -309,3 +314,131 @@ class GraphService(ServiceManager[GraphServer]):
|
|
|
309
314
|
await asyncio.wait_for(reader.read(1), timeout=timeout) # Complete
|
|
310
315
|
await close_stream_writer(writer)
|
|
311
316
|
return dag
|
|
317
|
+
|
|
318
|
+
async def get_pruned_graph(self) -> typing.Optional[defaultdict[str, set[str]]]:
|
|
319
|
+
try:
|
|
320
|
+
dag: DAG = await self.dag()
|
|
321
|
+
except (ConnectionRefusedError, ConnectionResetError):
|
|
322
|
+
logger.info(
|
|
323
|
+
f"GraphServer not running @{self.address}, or host is refusing connections"
|
|
324
|
+
)
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
graph_connections = dag.graph.copy()
|
|
328
|
+
# Let's eliminate proxy topics, i.e. connections with inputs and outputs.
|
|
329
|
+
source_nodes = []
|
|
330
|
+
for node, conns in graph_connections.items():
|
|
331
|
+
if len(conns) > 0:
|
|
332
|
+
source_nodes += [node]
|
|
333
|
+
proxy_topics = []
|
|
334
|
+
for conns in graph_connections.values():
|
|
335
|
+
for conn in conns:
|
|
336
|
+
if conn in source_nodes and conn not in proxy_topics:
|
|
337
|
+
proxy_topics += [conn]
|
|
338
|
+
# Replace Proxy Topics with actual source and downstream
|
|
339
|
+
for proxy_topic in proxy_topics:
|
|
340
|
+
downstreams = graph_connections.pop(proxy_topic)
|
|
341
|
+
logger.info(f"{proxy_topic} downstream connections: {downstreams}")
|
|
342
|
+
for node, conns in graph_connections.items():
|
|
343
|
+
for conn in conns:
|
|
344
|
+
if conn == proxy_topic:
|
|
345
|
+
new_conns = conns.copy()
|
|
346
|
+
new_conns.remove(proxy_topic)
|
|
347
|
+
new_conns.union(downstreams)
|
|
348
|
+
logger.info(
|
|
349
|
+
f"Updating connections for {node} from {conns} to {new_conns}"
|
|
350
|
+
)
|
|
351
|
+
graph_connections[node] = new_conns
|
|
352
|
+
|
|
353
|
+
return dag.graph.copy()
|
|
354
|
+
|
|
355
|
+
async def get_formatted_graph(self, fmt: str, direction: str = "LR") -> str:
|
|
356
|
+
if fmt not in ["mermaid", "graphviz"]:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"Invalid format '{fmt}'. Options are 'mermaid' or 'graphviz'"
|
|
359
|
+
)
|
|
360
|
+
graph_connections = await self.get_pruned_graph()
|
|
361
|
+
|
|
362
|
+
if graph_connections is None or not graph_connections:
|
|
363
|
+
return ""
|
|
364
|
+
|
|
365
|
+
# Let's come up with UUID node names
|
|
366
|
+
nodes = set(graph_connections.keys())
|
|
367
|
+
if fmt == "mermaid":
|
|
368
|
+
node_map = {name: f"{str(uuid4())}" for name in nodes}
|
|
369
|
+
else:
|
|
370
|
+
node_map = {name: f'"{str(uuid4())}"' for name in nodes} # graphviz
|
|
371
|
+
|
|
372
|
+
# Construct the graph
|
|
373
|
+
def tree():
|
|
374
|
+
return defaultdict(tree)
|
|
375
|
+
|
|
376
|
+
graph: defaultdict = tree()
|
|
377
|
+
|
|
378
|
+
connections = ""
|
|
379
|
+
for node, conns in graph_connections.items():
|
|
380
|
+
subgraph = graph
|
|
381
|
+
path = node.split("/")
|
|
382
|
+
route = path[:-1]
|
|
383
|
+
stream = path[-1]
|
|
384
|
+
for seg in route:
|
|
385
|
+
subgraph = subgraph[seg]
|
|
386
|
+
subgraph[stream] = node
|
|
387
|
+
|
|
388
|
+
for sub in conns:
|
|
389
|
+
if fmt == "mermaid":
|
|
390
|
+
connections += f"{node_map[node]} --> {node_map[sub]}" + "\n"
|
|
391
|
+
else:
|
|
392
|
+
connections += f"{node_map[node]} -> {node_map[sub]};" + "\n"
|
|
393
|
+
|
|
394
|
+
if fmt == "graphviz":
|
|
395
|
+
header = [
|
|
396
|
+
"digraph EZ {",
|
|
397
|
+
indent(f'rankdir="{direction}"', IND),
|
|
398
|
+
]
|
|
399
|
+
footer = ["}"]
|
|
400
|
+
|
|
401
|
+
def per_leaf(g, leaf):
|
|
402
|
+
out = None
|
|
403
|
+
if isinstance(g[leaf], defaultdict):
|
|
404
|
+
out = [
|
|
405
|
+
f"subgraph {leaf.lower()} {{",
|
|
406
|
+
indent("cluster = true;", IND),
|
|
407
|
+
indent(f'label = "{leaf}";', IND),
|
|
408
|
+
f"{recurse_graph(g[leaf])}}};",
|
|
409
|
+
]
|
|
410
|
+
elif isinstance(g[leaf], str):
|
|
411
|
+
out = [f"{node_map[g[leaf]]} [label={leaf}];"]
|
|
412
|
+
return out
|
|
413
|
+
|
|
414
|
+
else: # fmt == mermaid
|
|
415
|
+
header = [f"flowchart {direction}"]
|
|
416
|
+
footer = []
|
|
417
|
+
|
|
418
|
+
def per_leaf(g, leaf):
|
|
419
|
+
out = None
|
|
420
|
+
if isinstance(g[leaf], defaultdict):
|
|
421
|
+
out = [
|
|
422
|
+
f"subgraph {leaf.lower()} [{leaf}]",
|
|
423
|
+
# "direction LR",
|
|
424
|
+
f"{recurse_graph(g[leaf])}",
|
|
425
|
+
"end",
|
|
426
|
+
]
|
|
427
|
+
elif isinstance(g[leaf], str):
|
|
428
|
+
out = [f"{node_map[g[leaf]]}[{leaf}]"]
|
|
429
|
+
return out
|
|
430
|
+
|
|
431
|
+
def recurse_graph(g: defaultdict):
|
|
432
|
+
out = ""
|
|
433
|
+
for leaf in g:
|
|
434
|
+
leaf_list = per_leaf(g, leaf)
|
|
435
|
+
if leaf_list is not None:
|
|
436
|
+
leaf_list += [""] # Append a newline
|
|
437
|
+
out += indent("\n".join(leaf_list), IND)
|
|
438
|
+
return out[:-1]
|
|
439
|
+
|
|
440
|
+
graph_out = "\n".join(
|
|
441
|
+
header + [recurse_graph(graph), indent(connections, IND)] + footer
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
return graph_out
|
|
@@ -177,6 +177,7 @@ def create_socket(
|
|
|
177
177
|
ignore_ports: typing.List[int] = RESERVED_PORTS,
|
|
178
178
|
) -> socket.socket:
|
|
179
179
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
180
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
180
181
|
|
|
181
182
|
if host is None:
|
|
182
183
|
host = DEFAULT_HOST
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
|
+
import typing
|
|
3
4
|
|
|
4
5
|
from io import TextIOWrapper
|
|
5
6
|
from dataclasses import field
|
|
@@ -8,11 +9,10 @@ from pathlib import Path
|
|
|
8
9
|
import ezmsg.core as ez
|
|
9
10
|
|
|
10
11
|
from .messagecodec import MessageEncoder, LogStart
|
|
12
|
+
from .rate import Rate
|
|
11
13
|
|
|
12
|
-
from typing import Optional, Any, Dict, AsyncGenerator
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
def log_object(obj: Any) -> str:
|
|
15
|
+
def log_object(obj: typing.Any) -> str:
|
|
16
16
|
return json.dumps({"ts": time.time(), "obj": obj}, cls=MessageEncoder)
|
|
17
17
|
|
|
18
18
|
|
|
@@ -23,13 +23,18 @@ class MessageLoggerSettings(ez.Settings):
|
|
|
23
23
|
Args:
|
|
24
24
|
output: :py:class:`pathlib.Path` for a file where the messages will be logged.
|
|
25
25
|
If the file path already exists, the existing file will be truncated to 0 length.
|
|
26
|
+
write_period: Period in seconds for performing a write to disk.
|
|
27
|
+
If <=0, write incoming messages to disk immediately, applying backpressure as needed.
|
|
28
|
+
If >0, messages are buffered and periodically written to disk in a separate task
|
|
26
29
|
"""
|
|
27
30
|
|
|
28
|
-
output: Optional[Path] = None
|
|
31
|
+
output: typing.Optional[Path] = None
|
|
32
|
+
write_period: float = 0.0 # sec
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
class MessageLoggerState(ez.State):
|
|
32
|
-
output_files: Dict[Path, TextIOWrapper] = field(default_factory=dict)
|
|
36
|
+
output_files: typing.Dict[Path, TextIOWrapper] = field(default_factory=dict)
|
|
37
|
+
write_buffer: typing.List[str] = field(default_factory=list)
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
class MessageLogger(ez.Unit):
|
|
@@ -55,10 +60,10 @@ class MessageLogger(ez.Unit):
|
|
|
55
60
|
to stop logging messages to that path.
|
|
56
61
|
"""
|
|
57
62
|
|
|
58
|
-
INPUT_MESSAGE = ez.InputStream(Any)
|
|
63
|
+
INPUT_MESSAGE = ez.InputStream(typing.Any)
|
|
59
64
|
"""Pass a piece of data to log it to every open file which the ``MessageLogger`` is using."""
|
|
60
65
|
|
|
61
|
-
OUTPUT_MESSAGE = ez.OutputStream(Any)
|
|
66
|
+
OUTPUT_MESSAGE = ez.OutputStream(typing.Any)
|
|
62
67
|
"""Messages which are sent to ``INPUT_MESSAGE`` will pass through and be published on ``OUTPUT_MESSAGE``."""
|
|
63
68
|
|
|
64
69
|
OUTPUT_START = ez.OutputStream(Path)
|
|
@@ -69,7 +74,7 @@ class MessageLogger(ez.Unit):
|
|
|
69
74
|
"""If a file passed to ``INPUT_STOP`` is successfully closed, its path will be published to
|
|
70
75
|
``OUTPUT_STOP``, otherwise ``None``."""
|
|
71
76
|
|
|
72
|
-
def open_file(self, filepath: Path) -> Optional[Path]:
|
|
77
|
+
def open_file(self, filepath: Path) -> typing.Optional[Path]:
|
|
73
78
|
"""Returns file path if file successfully opened, otherwise None"""
|
|
74
79
|
if filepath in self.STATE.output_files:
|
|
75
80
|
# If the file is already open, we return None
|
|
@@ -85,7 +90,7 @@ class MessageLogger(ez.Unit):
|
|
|
85
90
|
|
|
86
91
|
return filepath
|
|
87
92
|
|
|
88
|
-
def close_file(self, filepath: Path) -> Optional[Path]:
|
|
93
|
+
def close_file(self, filepath: Path) -> typing.Optional[Path]:
|
|
89
94
|
"""Returns file path if file successfully closed, otherwise None"""
|
|
90
95
|
if filepath not in self.STATE.output_files:
|
|
91
96
|
# We haven't opened this file
|
|
@@ -103,28 +108,53 @@ class MessageLogger(ez.Unit):
|
|
|
103
108
|
|
|
104
109
|
@ez.subscriber(INPUT_START)
|
|
105
110
|
@ez.publisher(OUTPUT_START)
|
|
106
|
-
async def start_file(self, message: Path) -> AsyncGenerator:
|
|
111
|
+
async def start_file(self, message: Path) -> typing.AsyncGenerator:
|
|
107
112
|
out = self.open_file(message)
|
|
108
113
|
if out is not None:
|
|
109
114
|
yield (self.OUTPUT_START, out)
|
|
110
115
|
|
|
111
116
|
@ez.subscriber(INPUT_STOP)
|
|
112
117
|
@ez.publisher(OUTPUT_STOP)
|
|
113
|
-
async def stop_file(self, message: Path) -> AsyncGenerator:
|
|
118
|
+
async def stop_file(self, message: Path) -> typing.AsyncGenerator:
|
|
114
119
|
out = self.close_file(message)
|
|
115
120
|
if out is not None:
|
|
116
121
|
yield (self.OUTPUT_STOP, out)
|
|
117
122
|
|
|
118
123
|
@ez.subscriber(INPUT_MESSAGE)
|
|
119
124
|
@ez.publisher(OUTPUT_MESSAGE)
|
|
120
|
-
async def on_message(self, message: Any) -> AsyncGenerator:
|
|
121
|
-
strmessage = log_object(message)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
output_f.
|
|
125
|
+
async def on_message(self, message: typing.Any) -> typing.AsyncGenerator:
|
|
126
|
+
strmessage = f"{log_object(message)}\n"
|
|
127
|
+
|
|
128
|
+
if self.SETTINGS.write_period <= 0:
|
|
129
|
+
for output_f in self.STATE.output_files.values():
|
|
130
|
+
output_f.write(strmessage)
|
|
131
|
+
output_f.flush()
|
|
132
|
+
else:
|
|
133
|
+
self.STATE.write_buffer.append(strmessage)
|
|
134
|
+
|
|
125
135
|
yield (self.OUTPUT_MESSAGE, message)
|
|
126
136
|
|
|
137
|
+
@ez.task
|
|
138
|
+
async def buffered_write(self) -> None:
|
|
139
|
+
if self.SETTINGS.write_period <= 0:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
rate = Rate(1.0 / self.SETTINGS.write_period)
|
|
143
|
+
while True:
|
|
144
|
+
for output_f in self.STATE.output_files.values():
|
|
145
|
+
output_f.writelines(self.STATE.write_buffer)
|
|
146
|
+
output_f.flush()
|
|
147
|
+
self.STATE.write_buffer.clear()
|
|
148
|
+
await rate.sleep()
|
|
149
|
+
|
|
127
150
|
async def shutdown(self) -> None:
|
|
128
151
|
"""Note that files that are closed at shutdown don't publish messages"""
|
|
152
|
+
|
|
153
|
+
if len(self.STATE.write_buffer):
|
|
154
|
+
for output_f in self.STATE.output_files.values():
|
|
155
|
+
output_f.writelines(self.STATE.write_buffer)
|
|
156
|
+
output_f.flush()
|
|
157
|
+
self.STATE.write_buffer.clear()
|
|
158
|
+
|
|
129
159
|
for filepath in list(self.STATE.output_files):
|
|
130
160
|
self.close_file(filepath)
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
from abc import abstractmethod, ABC
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from dataclasses import field, dataclass
|
|
1
4
|
import math
|
|
2
5
|
import typing
|
|
3
6
|
import warnings
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
from contextlib import contextmanager
|
|
7
|
-
from dataclasses import field, dataclass
|
|
8
|
+
import ezmsg.core as ez
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
import numpy
|
|
11
|
-
import numpy.
|
|
10
|
+
try:
|
|
11
|
+
import numpy as np
|
|
12
|
+
import numpy.typing as npt
|
|
13
|
+
import numpy.lib.stride_tricks as nps
|
|
14
|
+
except ModuleNotFoundError:
|
|
15
|
+
ez.logger.error("Install ezmsg with the AxisArray extra:"
|
|
16
|
+
'pip install "ezmsg[AxisArray]"')
|
|
17
|
+
raise
|
|
12
18
|
|
|
13
19
|
from ezmsg.core.util import either_dict_or_kwargs
|
|
14
20
|
from .util import replace
|
|
15
21
|
|
|
22
|
+
|
|
16
23
|
if typing.TYPE_CHECKING:
|
|
17
24
|
try:
|
|
18
25
|
from xarray import DataArray
|
|
@@ -240,6 +240,9 @@ dependencies = [
|
|
|
240
240
|
]
|
|
241
241
|
|
|
242
242
|
[package.optional-dependencies]
|
|
243
|
+
axisarray = [
|
|
244
|
+
{ name = "numpy" },
|
|
245
|
+
]
|
|
243
246
|
docs = [
|
|
244
247
|
{ name = "ezmsg-sigproc" },
|
|
245
248
|
{ name = "sphinx" },
|
|
@@ -258,7 +261,8 @@ test = [
|
|
|
258
261
|
requires-dist = [
|
|
259
262
|
{ name = "ezmsg-sigproc", marker = "extra == 'docs'", specifier = ">=1.2.3" },
|
|
260
263
|
{ name = "flake8", marker = "extra == 'test'", specifier = ">=5.0.4" },
|
|
261
|
-
{ name = "numpy", marker = "extra == '
|
|
264
|
+
{ name = "numpy", marker = "extra == 'axisarray'", specifier = ">=1.24.4" },
|
|
265
|
+
{ name = "numpy", marker = "extra == 'test'", specifier = ">=1.24.4" },
|
|
262
266
|
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" },
|
|
263
267
|
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.23.8" },
|
|
264
268
|
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0" },
|
ezmsg-3.6.0/.envrc
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import base64
|
|
4
|
-
import asyncio
|
|
5
|
-
import argparse
|
|
6
|
-
import logging
|
|
7
|
-
import subprocess
|
|
8
|
-
import typing
|
|
9
|
-
import webbrowser
|
|
10
|
-
|
|
11
|
-
from collections import defaultdict
|
|
12
|
-
from uuid import uuid4
|
|
13
|
-
from textwrap import indent
|
|
14
|
-
|
|
15
|
-
from .graphserver import GraphService
|
|
16
|
-
from .shmserver import SHMService
|
|
17
|
-
from .netprotocol import (
|
|
18
|
-
Address,
|
|
19
|
-
GRAPHSERVER_ADDR_ENV,
|
|
20
|
-
GRAPHSERVER_PORT_DEFAULT,
|
|
21
|
-
SHMSERVER_ADDR_ENV,
|
|
22
|
-
SHMSERVER_PORT_DEFAULT,
|
|
23
|
-
PUBLISHER_START_PORT_ENV,
|
|
24
|
-
PUBLISHER_START_PORT_DEFAULT,
|
|
25
|
-
close_stream_writer,
|
|
26
|
-
)
|
|
27
|
-
from .dag import DAG
|
|
28
|
-
|
|
29
|
-
logger = logging.getLogger("ezmsg")
|
|
30
|
-
|
|
31
|
-
IND = " "
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def cmdline() -> None:
|
|
35
|
-
parser = argparse.ArgumentParser(
|
|
36
|
-
"ezmsg.core",
|
|
37
|
-
description="start and stop core ezmsg server processes",
|
|
38
|
-
epilog=f"""
|
|
39
|
-
You can also change server configuration with environment variables.
|
|
40
|
-
GraphServer will be hosted on ${GRAPHSERVER_ADDR_ENV} (default port: {GRAPHSERVER_PORT_DEFAULT}).
|
|
41
|
-
SHMServer will be hosted on ${SHMSERVER_ADDR_ENV} (default port: {SHMSERVER_PORT_DEFAULT}).
|
|
42
|
-
Publishers will be assigned available ports starting from {PUBLISHER_START_PORT_DEFAULT}. (Change with ${PUBLISHER_START_PORT_ENV})
|
|
43
|
-
""",
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
parser.add_argument(
|
|
47
|
-
"command",
|
|
48
|
-
help="command for ezmsg",
|
|
49
|
-
choices=["serve", "start", "shutdown", "graphviz", "mermaid"],
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
parser.add_argument("--address", help="Address for GraphServer", default=None)
|
|
53
|
-
|
|
54
|
-
class Args:
|
|
55
|
-
command: str
|
|
56
|
-
address: typing.Optional[str]
|
|
57
|
-
|
|
58
|
-
args = parser.parse_args(namespace=Args)
|
|
59
|
-
|
|
60
|
-
graph_address = Address("127.0.0.1", GRAPHSERVER_PORT_DEFAULT)
|
|
61
|
-
if args.address is not None:
|
|
62
|
-
graph_address = Address.from_string(args.address)
|
|
63
|
-
shm_address_str = os.environ.get(
|
|
64
|
-
SHMSERVER_ADDR_ENV, f"127.0.0.1:{SHMSERVER_PORT_DEFAULT}"
|
|
65
|
-
)
|
|
66
|
-
shm_address = Address.from_string(shm_address_str)
|
|
67
|
-
|
|
68
|
-
loop = asyncio.new_event_loop()
|
|
69
|
-
asyncio.set_event_loop(loop)
|
|
70
|
-
|
|
71
|
-
loop.run_until_complete(run_command(args.command, graph_address, shm_address))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async def run_command(cmd: str, graph_address: Address, shm_address: Address) -> None:
|
|
75
|
-
shm_service = SHMService(shm_address)
|
|
76
|
-
graph_service = GraphService(graph_address)
|
|
77
|
-
|
|
78
|
-
if cmd == "serve":
|
|
79
|
-
logger.info(f"GraphServer Address: {graph_address}")
|
|
80
|
-
logger.info(f"SHMServer Address: {shm_address}")
|
|
81
|
-
|
|
82
|
-
shm_server = shm_service.create_server()
|
|
83
|
-
graph_server = graph_service.create_server()
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
logger.info("Servers running...")
|
|
87
|
-
graph_server.join()
|
|
88
|
-
|
|
89
|
-
except KeyboardInterrupt:
|
|
90
|
-
logger.info("Interrupt detected; shutting down servers")
|
|
91
|
-
|
|
92
|
-
finally:
|
|
93
|
-
if graph_server is not None:
|
|
94
|
-
graph_server.stop()
|
|
95
|
-
|
|
96
|
-
if shm_server is not None:
|
|
97
|
-
shm_server.stop()
|
|
98
|
-
|
|
99
|
-
elif cmd == "start":
|
|
100
|
-
popen = subprocess.Popen(
|
|
101
|
-
[sys.executable, "-m", "ezmsg.core", "serve", f"--address={graph_address}"]
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
while True:
|
|
105
|
-
try:
|
|
106
|
-
_, writer = await graph_service.open_connection()
|
|
107
|
-
await close_stream_writer(writer)
|
|
108
|
-
_, writer = await shm_service.open_connection()
|
|
109
|
-
await close_stream_writer(writer)
|
|
110
|
-
break
|
|
111
|
-
except ConnectionRefusedError:
|
|
112
|
-
await asyncio.sleep(0.1)
|
|
113
|
-
|
|
114
|
-
logger.info(f"Forked ezmsg servers in PID: {popen.pid}")
|
|
115
|
-
|
|
116
|
-
elif cmd == "shutdown":
|
|
117
|
-
try:
|
|
118
|
-
await graph_service.shutdown()
|
|
119
|
-
logger.info(
|
|
120
|
-
f"Issued shutdown command to GraphServer @ {graph_service.address}"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
except ConnectionRefusedError:
|
|
124
|
-
logger.warning(
|
|
125
|
-
f"Could not issue shutdown command to GraphServer @ {graph_service.address}; server not running?"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
elif cmd == "graphviz":
|
|
129
|
-
try:
|
|
130
|
-
dag: DAG = await graph_service.dag()
|
|
131
|
-
except (ConnectionRefusedError, ConnectionResetError):
|
|
132
|
-
logger.info(
|
|
133
|
-
f"GraphServer not running @{graph_address}, or host is refusing connections"
|
|
134
|
-
)
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
graph_connections = dag.graph.copy()
|
|
138
|
-
# Let's eliminate proxy topics, i.e. connections with inputs and outputs.
|
|
139
|
-
source_nodes = []
|
|
140
|
-
for node, conns in graph_connections.items():
|
|
141
|
-
if len(conns) > 0:
|
|
142
|
-
source_nodes += [node]
|
|
143
|
-
proxy_topics = []
|
|
144
|
-
for conns in graph_connections.values():
|
|
145
|
-
for conn in conns:
|
|
146
|
-
if conn in source_nodes and conn not in proxy_topics:
|
|
147
|
-
proxy_topics += [conn]
|
|
148
|
-
# Replace Proxy Topics with actual source and downstream
|
|
149
|
-
for proxy_topic in proxy_topics:
|
|
150
|
-
downstreams = graph_connections.pop(proxy_topic)
|
|
151
|
-
logger.info(f"{proxy_topic} downstream connetions: {downstreams}")
|
|
152
|
-
for node, conns in graph_connections.items():
|
|
153
|
-
for conn in conns:
|
|
154
|
-
if conn == proxy_topic:
|
|
155
|
-
new_conns = conns.copy()
|
|
156
|
-
new_conns.remove(proxy_topic)
|
|
157
|
-
new_conns.union(downstreams)
|
|
158
|
-
logger.info(
|
|
159
|
-
f"Updating connections for {node} from {conns} to {new_conns}"
|
|
160
|
-
)
|
|
161
|
-
graph_connections[node] = new_conns
|
|
162
|
-
|
|
163
|
-
graph_connections = dag.graph.copy()
|
|
164
|
-
# Let's come up with UUID node names
|
|
165
|
-
nodes = set(graph_connections.keys())
|
|
166
|
-
node_map = {name: f'"{str(uuid4())}"' for name in nodes}
|
|
167
|
-
|
|
168
|
-
# Construct the graph
|
|
169
|
-
def tree():
|
|
170
|
-
return defaultdict(tree)
|
|
171
|
-
|
|
172
|
-
graph: defaultdict = tree()
|
|
173
|
-
|
|
174
|
-
connections = ""
|
|
175
|
-
for node, conns in graph_connections.items():
|
|
176
|
-
subgraph = graph
|
|
177
|
-
path = node.split("/")
|
|
178
|
-
route = path[:-1]
|
|
179
|
-
stream = path[-1]
|
|
180
|
-
for seg in route:
|
|
181
|
-
subgraph = subgraph[seg]
|
|
182
|
-
subgraph[stream] = node
|
|
183
|
-
|
|
184
|
-
for sub in conns:
|
|
185
|
-
connections += f"{node_map[node]} -> {node_map[sub]};" + "\n"
|
|
186
|
-
|
|
187
|
-
# Now convert to dot syntax
|
|
188
|
-
def recurse_graph(g: defaultdict):
|
|
189
|
-
out = ""
|
|
190
|
-
for leaf in g:
|
|
191
|
-
if isinstance(g[leaf], defaultdict):
|
|
192
|
-
out += indent(
|
|
193
|
-
"\n".join(
|
|
194
|
-
[
|
|
195
|
-
f"subgraph {leaf.lower()} {{",
|
|
196
|
-
indent("cluster = true;", IND),
|
|
197
|
-
indent(f'label = "{leaf}";', IND),
|
|
198
|
-
f"{recurse_graph(g[leaf])}}};",
|
|
199
|
-
"",
|
|
200
|
-
]
|
|
201
|
-
),
|
|
202
|
-
IND,
|
|
203
|
-
)
|
|
204
|
-
elif isinstance(g[leaf], str):
|
|
205
|
-
out += indent(f"{node_map[g[leaf]]} [label={leaf}];" + "\n", IND)
|
|
206
|
-
return out
|
|
207
|
-
|
|
208
|
-
subgraph_tree = recurse_graph(graph)
|
|
209
|
-
|
|
210
|
-
graphviz_out = "\n".join(
|
|
211
|
-
[
|
|
212
|
-
"digraph EZ {",
|
|
213
|
-
indent('rankdir="LR"', IND),
|
|
214
|
-
subgraph_tree,
|
|
215
|
-
indent(connections, IND),
|
|
216
|
-
"}",
|
|
217
|
-
]
|
|
218
|
-
)
|
|
219
|
-
|
|
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 isinstance(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}"
|
|
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
|
|
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
|
|
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
|
|
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
|