compas-eve 0.1.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.
- compas_eve-0.1.2/AUTHORS.md +3 -0
- compas_eve-0.1.2/CHANGELOG.md +24 -0
- compas_eve-0.1.2/LICENSE +21 -0
- compas_eve-0.1.2/MANIFEST.in +22 -0
- compas_eve-0.1.2/PKG-INFO +118 -0
- compas_eve-0.1.2/README.md +92 -0
- compas_eve-0.1.2/pyproject.toml +27 -0
- compas_eve-0.1.2/requirements.txt +2 -0
- compas_eve-0.1.2/setup.cfg +18 -0
- compas_eve-0.1.2/setup.py +66 -0
- compas_eve-0.1.2/src/compas_eve/__init__.py +46 -0
- compas_eve-0.1.2/src/compas_eve/__main__.py +2 -0
- compas_eve-0.1.2/src/compas_eve/core.py +155 -0
- compas_eve-0.1.2/src/compas_eve/event_emitter.py +244 -0
- compas_eve-0.1.2/src/compas_eve/memory/__init__.py +52 -0
- compas_eve-0.1.2/src/compas_eve/mqtt/__init__.py +8 -0
- compas_eve-0.1.2/src/compas_eve/mqtt/mqtt_cli.py +107 -0
- compas_eve-0.1.2/src/compas_eve/mqtt/mqtt_paho.py +70 -0
- compas_eve-0.1.2/src/compas_eve/mqtt/netlib/MQTTnet.dll +0 -0
- compas_eve-0.1.2/src/compas_eve/rhino/__init__.py +0 -0
- compas_eve-0.1.2/src/compas_eve/rhino/install.py +11 -0
- compas_eve-0.1.2/src/compas_eve.egg-info/PKG-INFO +118 -0
- compas_eve-0.1.2/src/compas_eve.egg-info/SOURCES.txt +26 -0
- compas_eve-0.1.2/src/compas_eve.egg-info/dependency_links.txt +1 -0
- compas_eve-0.1.2/src/compas_eve.egg-info/not-zip-safe +1 -0
- compas_eve-0.1.2/src/compas_eve.egg-info/requires.txt +2 -0
- compas_eve-0.1.2/src/compas_eve.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.2] 2023-07-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.1.1] 2023-07-09
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
compas_eve-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Gramazio Kohler Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
graft src
|
|
2
|
+
|
|
3
|
+
prune .github
|
|
4
|
+
prune data
|
|
5
|
+
prune docs
|
|
6
|
+
prune scripts
|
|
7
|
+
prune tests
|
|
8
|
+
prune temp
|
|
9
|
+
|
|
10
|
+
include LICENSE
|
|
11
|
+
include README.md
|
|
12
|
+
include AUTHORS.md
|
|
13
|
+
include CHANGELOG.md
|
|
14
|
+
include requirements.txt
|
|
15
|
+
|
|
16
|
+
exclude requirements-dev.txt
|
|
17
|
+
exclude pytest.ini .bumpversion.cfg .editorconfig
|
|
18
|
+
exclude tasks.py
|
|
19
|
+
exclude CONTRIBUTING.md
|
|
20
|
+
exclude conftest.py
|
|
21
|
+
|
|
22
|
+
global-exclude *.py[cod] __pycache__ *.dylib *.nb[ic] .DS_Store
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: compas_eve
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: COMPAS Event Extensions: adds event-based communication infrastructure to the COMPAS framework.
|
|
5
|
+
Home-page: https://github.com/compas-dev/compas_eve
|
|
6
|
+
Author: Gonzalo Casas
|
|
7
|
+
Author-email: casas@arch.ethz.ch
|
|
8
|
+
License: MIT license
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: Unix
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
License-File: AUTHORS.md
|
|
26
|
+
|
|
27
|
+
# compas_eve
|
|
28
|
+
|
|
29
|
+
COMPAS Event Extensions: adds event-based communication infrastructure to the COMPAS framework.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Getting started with this project
|
|
33
|
+
|
|
34
|
+
### Setup code editor
|
|
35
|
+
|
|
36
|
+
1. Open project folder in VS Code
|
|
37
|
+
2. Select python environment for the project
|
|
38
|
+
3. First time using VS Code and on Windows? Make sure select the correct terminal profile: `Ctrl+Shift+P`, `Terminal: Select Default Profile` and select `Command Prompt`.
|
|
39
|
+
|
|
40
|
+
> All terminal commands in the following sections can be run from the VS Code integrated terminal.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### First steps with git
|
|
44
|
+
|
|
45
|
+
1. Go to the `Source control` tab
|
|
46
|
+
2. Make an initial commit with all newly created files
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### First steps with code
|
|
50
|
+
|
|
51
|
+
1. Install the newly created project
|
|
52
|
+
|
|
53
|
+
pip install -e .
|
|
54
|
+
|
|
55
|
+
2. Install it on Rhino
|
|
56
|
+
|
|
57
|
+
python -m compas_rhino.install
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Code conventions
|
|
61
|
+
|
|
62
|
+
Code convention follows [PEP8](https://pep8.org/) style guidelines and line length of 120 characters.
|
|
63
|
+
|
|
64
|
+
1. Check adherence to style guidelines
|
|
65
|
+
|
|
66
|
+
invoke lint
|
|
67
|
+
|
|
68
|
+
2. Format code automatically
|
|
69
|
+
|
|
70
|
+
invoke format
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
### Documentation
|
|
74
|
+
|
|
75
|
+
Documentation is generated automatically out of docstrings and [RST](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) files in this repository
|
|
76
|
+
|
|
77
|
+
1. Generate the docs
|
|
78
|
+
|
|
79
|
+
invoke docs
|
|
80
|
+
|
|
81
|
+
2. Check links in docs are valid
|
|
82
|
+
|
|
83
|
+
invoke linkcheck
|
|
84
|
+
|
|
85
|
+
3. Open docs in your browser (file explorer -> `dist/docs/index.html`)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
### Testing
|
|
89
|
+
|
|
90
|
+
Tests are written using the [pytest](https://docs.pytest.org/) framework
|
|
91
|
+
|
|
92
|
+
1. Run all tests from terminal
|
|
93
|
+
|
|
94
|
+
invoke test
|
|
95
|
+
|
|
96
|
+
2. Or run them from VS Code from the `Testing` tab
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### Developing Grasshopper components
|
|
100
|
+
|
|
101
|
+
We use [Grasshopper Componentizer](https://github.com/compas-dev/compas-actions.ghpython_components) to develop Python components that can be stored and edited on git.
|
|
102
|
+
|
|
103
|
+
1. Build components
|
|
104
|
+
|
|
105
|
+
invoke build-ghuser-components
|
|
106
|
+
|
|
107
|
+
2. Install components on Rhino
|
|
108
|
+
|
|
109
|
+
python -m compas_rhino.install
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
### Publish release
|
|
113
|
+
|
|
114
|
+
Releases follow the [semver](https://semver.org/spec/v2.0.0.html) versioning convention.
|
|
115
|
+
|
|
116
|
+
1. Create a new release
|
|
117
|
+
|
|
118
|
+
invoke release major
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# compas_eve
|
|
2
|
+
|
|
3
|
+
COMPAS Event Extensions: adds event-based communication infrastructure to the COMPAS framework.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Getting started with this project
|
|
7
|
+
|
|
8
|
+
### Setup code editor
|
|
9
|
+
|
|
10
|
+
1. Open project folder in VS Code
|
|
11
|
+
2. Select python environment for the project
|
|
12
|
+
3. First time using VS Code and on Windows? Make sure select the correct terminal profile: `Ctrl+Shift+P`, `Terminal: Select Default Profile` and select `Command Prompt`.
|
|
13
|
+
|
|
14
|
+
> All terminal commands in the following sections can be run from the VS Code integrated terminal.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### First steps with git
|
|
18
|
+
|
|
19
|
+
1. Go to the `Source control` tab
|
|
20
|
+
2. Make an initial commit with all newly created files
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### First steps with code
|
|
24
|
+
|
|
25
|
+
1. Install the newly created project
|
|
26
|
+
|
|
27
|
+
pip install -e .
|
|
28
|
+
|
|
29
|
+
2. Install it on Rhino
|
|
30
|
+
|
|
31
|
+
python -m compas_rhino.install
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Code conventions
|
|
35
|
+
|
|
36
|
+
Code convention follows [PEP8](https://pep8.org/) style guidelines and line length of 120 characters.
|
|
37
|
+
|
|
38
|
+
1. Check adherence to style guidelines
|
|
39
|
+
|
|
40
|
+
invoke lint
|
|
41
|
+
|
|
42
|
+
2. Format code automatically
|
|
43
|
+
|
|
44
|
+
invoke format
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### Documentation
|
|
48
|
+
|
|
49
|
+
Documentation is generated automatically out of docstrings and [RST](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) files in this repository
|
|
50
|
+
|
|
51
|
+
1. Generate the docs
|
|
52
|
+
|
|
53
|
+
invoke docs
|
|
54
|
+
|
|
55
|
+
2. Check links in docs are valid
|
|
56
|
+
|
|
57
|
+
invoke linkcheck
|
|
58
|
+
|
|
59
|
+
3. Open docs in your browser (file explorer -> `dist/docs/index.html`)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Testing
|
|
63
|
+
|
|
64
|
+
Tests are written using the [pytest](https://docs.pytest.org/) framework
|
|
65
|
+
|
|
66
|
+
1. Run all tests from terminal
|
|
67
|
+
|
|
68
|
+
invoke test
|
|
69
|
+
|
|
70
|
+
2. Or run them from VS Code from the `Testing` tab
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
### Developing Grasshopper components
|
|
74
|
+
|
|
75
|
+
We use [Grasshopper Componentizer](https://github.com/compas-dev/compas-actions.ghpython_components) to develop Python components that can be stored and edited on git.
|
|
76
|
+
|
|
77
|
+
1. Build components
|
|
78
|
+
|
|
79
|
+
invoke build-ghuser-components
|
|
80
|
+
|
|
81
|
+
2. Install components on Rhino
|
|
82
|
+
|
|
83
|
+
python -m compas_rhino.install
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
### Publish release
|
|
87
|
+
|
|
88
|
+
Releases follow the [semver](https://semver.org/spec/v2.0.0.html) versioning convention.
|
|
89
|
+
|
|
90
|
+
1. Create a new release
|
|
91
|
+
|
|
92
|
+
invoke release major
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[tool.black]
|
|
2
|
+
line-length = 120
|
|
3
|
+
|
|
4
|
+
[tool.pytest.ini_options]
|
|
5
|
+
minversion = "6.0"
|
|
6
|
+
testpaths = ["tests"]
|
|
7
|
+
python_files = [
|
|
8
|
+
"test_*.py",
|
|
9
|
+
"tests.py"
|
|
10
|
+
]
|
|
11
|
+
addopts = "-ra --strict --doctest-modules --doctest-glob=*.rst --tb=short"
|
|
12
|
+
doctest_optionflags= "NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ALLOW_UNICODE ALLOW_BYTES NUMBER"
|
|
13
|
+
filterwarnings = "ignore::DeprecationWarning"
|
|
14
|
+
|
|
15
|
+
[tool.isort]
|
|
16
|
+
line_length = 120
|
|
17
|
+
multi_line_output = 3
|
|
18
|
+
include_trailing_comma = true
|
|
19
|
+
force_grid_wrap = 0
|
|
20
|
+
use_parentheses = true
|
|
21
|
+
force_single_line = true
|
|
22
|
+
ensure_newline_before_comments = true
|
|
23
|
+
known_first_party = "compas_eve"
|
|
24
|
+
default_section = "THIRDPARTY"
|
|
25
|
+
forced_separate = "test_compas_eve"
|
|
26
|
+
skip = ["__init__.py"]
|
|
27
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- encoding: utf-8 -*-
|
|
3
|
+
# flake8: noqa
|
|
4
|
+
from __future__ import absolute_import
|
|
5
|
+
from __future__ import print_function
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
from os import path
|
|
9
|
+
|
|
10
|
+
from setuptools import setup
|
|
11
|
+
from setuptools.command.develop import develop
|
|
12
|
+
from setuptools.command.install import install
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
here = path.abspath(path.dirname(__file__))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read(*names, **kwargs):
|
|
19
|
+
return io.open(path.join(here, *names), encoding=kwargs.get("encoding", "utf8")).read()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
long_description = read("README.md")
|
|
23
|
+
requirements = read("requirements.txt").split("\n")
|
|
24
|
+
optional_requirements = {}
|
|
25
|
+
|
|
26
|
+
setup(
|
|
27
|
+
name="compas_eve",
|
|
28
|
+
version="0.1.2",
|
|
29
|
+
description="COMPAS Event Extensions: adds event-based communication infrastructure to the COMPAS framework.",
|
|
30
|
+
long_description=long_description,
|
|
31
|
+
long_description_content_type="text/markdown",
|
|
32
|
+
url="https://github.com/compas-dev/compas_eve",
|
|
33
|
+
author="Gonzalo Casas",
|
|
34
|
+
author_email="casas@arch.ethz.ch",
|
|
35
|
+
license="MIT license",
|
|
36
|
+
classifiers=[
|
|
37
|
+
"Development Status :: 4 - Beta",
|
|
38
|
+
"Intended Audience :: Developers",
|
|
39
|
+
"Topic :: Scientific/Engineering",
|
|
40
|
+
"License :: OSI Approved :: MIT License",
|
|
41
|
+
"Operating System :: Unix",
|
|
42
|
+
"Operating System :: POSIX",
|
|
43
|
+
"Operating System :: Microsoft :: Windows",
|
|
44
|
+
"Programming Language :: Python",
|
|
45
|
+
"Programming Language :: Python :: 3",
|
|
46
|
+
"Programming Language :: Python :: 3.8",
|
|
47
|
+
"Programming Language :: Python :: 3.9",
|
|
48
|
+
"Programming Language :: Python :: 3.10",
|
|
49
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
50
|
+
],
|
|
51
|
+
keywords=[],
|
|
52
|
+
project_urls={},
|
|
53
|
+
packages=["compas_eve"],
|
|
54
|
+
package_dir={"": "src"},
|
|
55
|
+
package_data={},
|
|
56
|
+
data_files=[],
|
|
57
|
+
include_package_data=True,
|
|
58
|
+
zip_safe=False,
|
|
59
|
+
install_requires=requirements,
|
|
60
|
+
python_requires=">=3.8",
|
|
61
|
+
extras_require=optional_requirements,
|
|
62
|
+
entry_points={
|
|
63
|
+
"console_scripts": [],
|
|
64
|
+
},
|
|
65
|
+
ext_modules=[],
|
|
66
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
********************************************************************************
|
|
3
|
+
compas_eve
|
|
4
|
+
********************************************************************************
|
|
5
|
+
|
|
6
|
+
.. currentmodule:: compas_eve
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
.. toctree::
|
|
10
|
+
:maxdepth: 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import print_function
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__author__ = ["Gonzalo Casas"]
|
|
21
|
+
__copyright__ = "Gramazio Kohler Research"
|
|
22
|
+
__license__ = "MIT License"
|
|
23
|
+
__email__ = "casas@arch.ethz.ch"
|
|
24
|
+
__version__ = "0.1.2"
|
|
25
|
+
|
|
26
|
+
from .event_emitter import EventEmitterMixin # noqa: F401 needed here to avoid circular import on py2.7
|
|
27
|
+
from .core import Message, Publisher, Subscriber, Transport, Topic, get_default_transport, set_default_transport
|
|
28
|
+
from .memory import InMemoryTransport
|
|
29
|
+
|
|
30
|
+
HERE = os.path.dirname(__file__)
|
|
31
|
+
HOME = os.path.abspath(os.path.join(HERE, "../../"))
|
|
32
|
+
|
|
33
|
+
set_default_transport(InMemoryTransport())
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"HOME",
|
|
37
|
+
"Message",
|
|
38
|
+
"Publisher",
|
|
39
|
+
"Subscriber",
|
|
40
|
+
"Topic",
|
|
41
|
+
"Transport",
|
|
42
|
+
"get_default_transport",
|
|
43
|
+
"set_default_transport",
|
|
44
|
+
"InMemoryTransport",
|
|
45
|
+
]
|
|
46
|
+
__all_plugins__ = ["compas_eve.rhino.install"]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
# Python 2/3 compatibility import list
|
|
4
|
+
try:
|
|
5
|
+
from collections import UserDict
|
|
6
|
+
except ImportError:
|
|
7
|
+
from UserDict import UserDict
|
|
8
|
+
|
|
9
|
+
DEFAULT_TRANSPORT = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_default_transport():
|
|
13
|
+
return DEFAULT_TRANSPORT
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_default_transport(transport):
|
|
17
|
+
global DEFAULT_TRANSPORT
|
|
18
|
+
DEFAULT_TRANSPORT = transport
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageEncoder(json.JSONEncoder):
|
|
22
|
+
"""Internal class to serialize some of the core data types into json."""
|
|
23
|
+
|
|
24
|
+
def default(self, o):
|
|
25
|
+
return super(MessageEncoder, self).default(o)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Transport(object):
|
|
29
|
+
"""Defines the base interface for different transport implementations."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, *args, **kwargs):
|
|
32
|
+
super(Transport, self).__init__(*args, **kwargs)
|
|
33
|
+
self._id_counter = 0
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def id_counter(self):
|
|
37
|
+
"""Generate an auto-incremental ID starting from 1."""
|
|
38
|
+
self._id_counter += 1
|
|
39
|
+
return self._id_counter
|
|
40
|
+
|
|
41
|
+
def publish(self, topic, message):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def subscribe(self, topic, callback):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def unsubscribe(self, topic):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def advertise(self, topic):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def unadvertise(self, topic):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Message(UserDict):
|
|
58
|
+
"""Message objects used for publishing and subscribing to/from topics.
|
|
59
|
+
|
|
60
|
+
A message is fundamentally a dictionary and behaves as one."""
|
|
61
|
+
|
|
62
|
+
def __getattr__(self, name):
|
|
63
|
+
return self.data[name]
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def parse(cls, value):
|
|
67
|
+
instance = cls()
|
|
68
|
+
instance.update(value)
|
|
69
|
+
return instance
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Topic(object):
|
|
73
|
+
"""Describes a topic"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, name, message_type, **options):
|
|
76
|
+
self.name = name
|
|
77
|
+
self.message_type = message_type
|
|
78
|
+
self.options = options
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Publisher(object):
|
|
82
|
+
"""Publisher interface."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, topic, transport=None):
|
|
85
|
+
self.topic = topic
|
|
86
|
+
self.transport = transport or get_default_transport()
|
|
87
|
+
self._advertise_id = None
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_advertised(self):
|
|
91
|
+
"""Indicate if the publisher has announce its topic as advertised or not.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
bool: True if advertised as publisher of this topic, False otherwise.
|
|
95
|
+
"""
|
|
96
|
+
return self._advertise_id is not None
|
|
97
|
+
|
|
98
|
+
def message_published(self, message):
|
|
99
|
+
"""Handler called when a message has been published."""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def publish(self, message):
|
|
103
|
+
"""Publish a message to a topic."""
|
|
104
|
+
# TODO: check if message type matches self.topic.message_type declared
|
|
105
|
+
if not self.is_advertised:
|
|
106
|
+
self.advertise()
|
|
107
|
+
|
|
108
|
+
self.transport.publish(self.topic, message)
|
|
109
|
+
self.message_published(message)
|
|
110
|
+
|
|
111
|
+
def advertise(self):
|
|
112
|
+
if self.is_advertised:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
self._advertise_id = self.transport.advertise(self.topic)
|
|
116
|
+
|
|
117
|
+
def unadvertise(self):
|
|
118
|
+
if not self.is_advertised:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
self.transport.unadvertise(self.topic)
|
|
122
|
+
self._advertise_id = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Subscriber(object):
|
|
126
|
+
"""Subscriber interface."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, topic, transport=None):
|
|
129
|
+
self.transport = transport or get_default_transport()
|
|
130
|
+
self.topic = topic
|
|
131
|
+
self._subscribe_id = None
|
|
132
|
+
|
|
133
|
+
# This is basically an abstract method that needs to be implemented in subclasses
|
|
134
|
+
# It's not really marked as such because IronPython causes a huge memory leak in these cases
|
|
135
|
+
def message_received(self, message):
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def is_subscribed(self):
|
|
140
|
+
"""Indicate if the instace is currently subscribed to its topic or not."""
|
|
141
|
+
return self._subscribe_id is not None
|
|
142
|
+
|
|
143
|
+
def subscribe(self):
|
|
144
|
+
if self._subscribe_id:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
self._subscribe_id = self.transport.subscribe(self.topic, self.message_received)
|
|
148
|
+
|
|
149
|
+
def unsubscribe(self):
|
|
150
|
+
"""Unregister the subscriber from its topic."""
|
|
151
|
+
if not self._subscribe_id:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
self.transport.unsubscribe(self.topic)
|
|
155
|
+
self._subscribe_id = None
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# The following code is derivative work of [pyee](https://github.com/jfhbrook/pyee)
|
|
4
|
+
# Copyrighted by Joshua Holbrook with a MIT license
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# The MIT License(MIT)
|
|
8
|
+
|
|
9
|
+
# Copyright(c) 2015 Joshua Holbrook
|
|
10
|
+
|
|
11
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
# of this software and associated documentation files(the "Software"), to deal
|
|
13
|
+
# in the Software without restriction, including without limitation the rights
|
|
14
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
|
15
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
# furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
# The above copyright notice and this permission notice shall be included in
|
|
19
|
+
# all copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
27
|
+
# THE SOFTWARE.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
``EventEmitterMixin`` is similar to the ``EventEmitter`` of Node.js.
|
|
31
|
+
It supports both synchronous callbacks and asyncio coroutines.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from asyncio import ensure_future
|
|
36
|
+
from asyncio import iscoroutine
|
|
37
|
+
except ImportError:
|
|
38
|
+
iscoroutine = None
|
|
39
|
+
ensure_future = None
|
|
40
|
+
|
|
41
|
+
from collections import OrderedDict
|
|
42
|
+
from collections import defaultdict
|
|
43
|
+
from threading import RLock
|
|
44
|
+
|
|
45
|
+
__all__ = ["EventEmitterMixin", "EventEmitterException"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class EventEmitterException(Exception):
|
|
49
|
+
"""An internal exception."""
|
|
50
|
+
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EventEmitterMixin(object):
|
|
55
|
+
"""Mixin to add event emitter features to a class.
|
|
56
|
+
|
|
57
|
+
For interoperation with asyncio, one can specify the scheduler and
|
|
58
|
+
the event loop. The scheduler defaults to ``asyncio.ensure_future``,
|
|
59
|
+
and the loop defaults to ``None``. When used with the default scheduler,
|
|
60
|
+
this will schedule the coroutine onto asyncio's default loop.
|
|
61
|
+
|
|
62
|
+
This should also be compatible with recent versions of twisted by
|
|
63
|
+
setting ``scheduler=twisted.internet.defer.ensureDeferred``.
|
|
64
|
+
|
|
65
|
+
Most events are registered with EventEmitterMixin via the ``on`` and ``once``
|
|
66
|
+
methods. However, event emitters have two *special* events:
|
|
67
|
+
|
|
68
|
+
- ``new_listener``: Fires whenever a new listener is created. Listeners for
|
|
69
|
+
this event do not fire upon their own creation.
|
|
70
|
+
|
|
71
|
+
- ``error``: When emitted raises an Exception by default, behavior can be
|
|
72
|
+
overriden by attaching callback to the event.
|
|
73
|
+
|
|
74
|
+
For example::
|
|
75
|
+
|
|
76
|
+
@ee.on('error')
|
|
77
|
+
def onError(message):
|
|
78
|
+
logging.err(message)
|
|
79
|
+
|
|
80
|
+
ee.emit('error', Exception('something blew up'))
|
|
81
|
+
|
|
82
|
+
For synchronous callbacks, exceptions are **not** handled for you---
|
|
83
|
+
you must catch your own exceptions inside synchronous ``on`` handlers.
|
|
84
|
+
However, when wrapping **async** functions, errors will be intercepted
|
|
85
|
+
and emitted under the ``error`` event. **This behavior for async
|
|
86
|
+
functions is inconsistent with node.js**, which unlike this package has
|
|
87
|
+
no facilities for handling returned Promises from handlers.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, *args, **kwargs):
|
|
91
|
+
super(EventEmitterMixin, self).__init__(*args, **kwargs)
|
|
92
|
+
self._events = defaultdict(OrderedDict)
|
|
93
|
+
self._schedule = kwargs.get("scheduler", ensure_future)
|
|
94
|
+
self._loop = kwargs.get("loop", None)
|
|
95
|
+
self._event_lock = RLock()
|
|
96
|
+
|
|
97
|
+
def on(self, event, f=None):
|
|
98
|
+
"""Registers the function (or optionally an asyncio coroutine function)
|
|
99
|
+
``f`` to the event name ``event``.
|
|
100
|
+
|
|
101
|
+
If ``f`` isn't provided, this method returns a function that
|
|
102
|
+
takes ``f`` as a callback; in other words, you can use this method
|
|
103
|
+
as a decorator, like so::
|
|
104
|
+
|
|
105
|
+
@ee.on('data')
|
|
106
|
+
def data_handler(data):
|
|
107
|
+
print(data)
|
|
108
|
+
|
|
109
|
+
As mentioned, this method can also take an asyncio coroutine function::
|
|
110
|
+
|
|
111
|
+
@ee.on('data')
|
|
112
|
+
async def data_handler(data)
|
|
113
|
+
await do_async_thing(data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
This will automatically schedule the coroutine using the configured
|
|
117
|
+
scheduling function (defaults to ``asyncio.ensure_future``) and the
|
|
118
|
+
configured event loop (defaults to ``asyncio.get_event_loop()``).
|
|
119
|
+
|
|
120
|
+
In both the decorated and undecorated forms, the event handler is
|
|
121
|
+
returned. The upshot of this is that you can call decorated handlers
|
|
122
|
+
directly, as well as use them in remove_listener calls.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
with self._event_lock:
|
|
126
|
+
|
|
127
|
+
def _on(f):
|
|
128
|
+
self._add_event_handler(event, f, f)
|
|
129
|
+
return f
|
|
130
|
+
|
|
131
|
+
if f is None:
|
|
132
|
+
return _on
|
|
133
|
+
else:
|
|
134
|
+
return _on(f)
|
|
135
|
+
|
|
136
|
+
def _add_event_handler(self, event, k, v):
|
|
137
|
+
# Fire 'new_listener' *before* adding the new listener!
|
|
138
|
+
self.emit("new_listener", event, k)
|
|
139
|
+
|
|
140
|
+
# Add the necessary function
|
|
141
|
+
# Note that k and v are the same for `on` handlers, but
|
|
142
|
+
# different for `once` handlers, where v is a wrapped version
|
|
143
|
+
# of k which removes itself before calling k
|
|
144
|
+
self._events[event][k] = v
|
|
145
|
+
|
|
146
|
+
def emit(self, event, *args, **kwargs):
|
|
147
|
+
"""Emit ``event``, passing ``*args`` and ``**kwargs`` to each attached
|
|
148
|
+
function. Returns ``True`` if any functions are attached to ``event``;
|
|
149
|
+
otherwise returns ``False``.
|
|
150
|
+
|
|
151
|
+
Example::
|
|
152
|
+
|
|
153
|
+
ee.emit('data', '00101001')
|
|
154
|
+
|
|
155
|
+
Assuming ``data`` is an attached function, this will call
|
|
156
|
+
``data('00101001')'``.
|
|
157
|
+
|
|
158
|
+
For coroutine event handlers, calling emit is non-blocking. In other
|
|
159
|
+
words, you do not have to await any results from emit, and the
|
|
160
|
+
coroutine is scheduled in a fire-and-forget fashion.
|
|
161
|
+
"""
|
|
162
|
+
handled = False
|
|
163
|
+
|
|
164
|
+
with self._event_lock:
|
|
165
|
+
for f in list(self._events[event].values()):
|
|
166
|
+
result = f(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
# If f was a coroutine function, we need to schedule it and
|
|
169
|
+
# handle potential errors
|
|
170
|
+
if iscoroutine and iscoroutine(result):
|
|
171
|
+
if self._loop:
|
|
172
|
+
d = self._schedule(result, loop=self._loop)
|
|
173
|
+
else:
|
|
174
|
+
d = self._schedule(result)
|
|
175
|
+
|
|
176
|
+
# scheduler gave us an asyncio Future
|
|
177
|
+
if hasattr(d, "add_done_callback"):
|
|
178
|
+
|
|
179
|
+
@d.add_done_callback
|
|
180
|
+
def _callback(f):
|
|
181
|
+
exc = f.exception()
|
|
182
|
+
if exc:
|
|
183
|
+
self.emit("error", exc)
|
|
184
|
+
|
|
185
|
+
# scheduler gave us a twisted Deferred
|
|
186
|
+
elif hasattr(d, "addErrback"):
|
|
187
|
+
|
|
188
|
+
@d.addErrback
|
|
189
|
+
def _callback(exc):
|
|
190
|
+
self.emit("error", exc)
|
|
191
|
+
|
|
192
|
+
handled = True
|
|
193
|
+
|
|
194
|
+
if not handled and event == "error":
|
|
195
|
+
if args:
|
|
196
|
+
raise args[0]
|
|
197
|
+
else:
|
|
198
|
+
raise EventEmitterException("Uncaught, unspecified 'error' event.")
|
|
199
|
+
|
|
200
|
+
return handled
|
|
201
|
+
|
|
202
|
+
def once(self, event, f=None):
|
|
203
|
+
"""The same as ``ee.on``, except that the listener is automatically
|
|
204
|
+
removed after being called.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
with self._event_lock:
|
|
208
|
+
|
|
209
|
+
def _wrapper(f):
|
|
210
|
+
def g(*args, **kwargs):
|
|
211
|
+
self.remove_listener(event, f)
|
|
212
|
+
# f may return a coroutine, so we need to return that
|
|
213
|
+
# result here so that emit can schedule it
|
|
214
|
+
return f(*args, **kwargs)
|
|
215
|
+
|
|
216
|
+
self._add_event_handler(event, f, g)
|
|
217
|
+
return f
|
|
218
|
+
|
|
219
|
+
if f is None:
|
|
220
|
+
return _wrapper
|
|
221
|
+
else:
|
|
222
|
+
return _wrapper(f)
|
|
223
|
+
|
|
224
|
+
def off(self, event, f):
|
|
225
|
+
"""Removes the function ``f`` from ``event``."""
|
|
226
|
+
self._events[event].pop(f)
|
|
227
|
+
|
|
228
|
+
def remove_listener(self, event, f):
|
|
229
|
+
"""Removes the function ``f`` from ``event``."""
|
|
230
|
+
self._events[event].pop(f)
|
|
231
|
+
|
|
232
|
+
def remove_all_listeners(self, event=None):
|
|
233
|
+
"""Remove all listeners attached to ``event``.
|
|
234
|
+
If ``event`` is ``None``, remove all listeners on all events.
|
|
235
|
+
"""
|
|
236
|
+
with self._event_lock:
|
|
237
|
+
if event is not None:
|
|
238
|
+
self._events[event] = OrderedDict()
|
|
239
|
+
else:
|
|
240
|
+
self._events = defaultdict(OrderedDict)
|
|
241
|
+
|
|
242
|
+
def listeners(self, event):
|
|
243
|
+
"""Returns a list of all listeners registered to the ``event``."""
|
|
244
|
+
return list(self._events[event].keys())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from compas_eve.event_emitter import EventEmitterMixin
|
|
2
|
+
from compas_eve.core import Transport
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InMemoryTransport(Transport, EventEmitterMixin):
|
|
6
|
+
"""In-Memory transport is ideal for simple single-process apps and testing.
|
|
7
|
+
|
|
8
|
+
It will only distribute messages within the same process, not across different processes."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, *args, **kwargs):
|
|
11
|
+
super(InMemoryTransport, self).__init__(*args, **kwargs)
|
|
12
|
+
|
|
13
|
+
def on_ready(self, callback):
|
|
14
|
+
"""In-memory transport is always ready, it will immediately trigger the callback."""
|
|
15
|
+
callback()
|
|
16
|
+
|
|
17
|
+
def publish(self, topic, message):
|
|
18
|
+
"""Publish a message to a topic."""
|
|
19
|
+
event_key = "event_{}".format(topic.name)
|
|
20
|
+
|
|
21
|
+
def _callback(**kwargs):
|
|
22
|
+
self.emit(event_key, message)
|
|
23
|
+
|
|
24
|
+
self.on_ready(_callback)
|
|
25
|
+
|
|
26
|
+
def subscribe(self, topic, callback):
|
|
27
|
+
"""Subscribe to be notified of messages on a given topic."""
|
|
28
|
+
event_key = "event_{}".format(topic.name)
|
|
29
|
+
|
|
30
|
+
def _callback(**kwargs):
|
|
31
|
+
self.on(event_key, callback)
|
|
32
|
+
|
|
33
|
+
self.on_ready(_callback)
|
|
34
|
+
|
|
35
|
+
def unsubscribe(self, topic):
|
|
36
|
+
"""Unsubscribe from the specified topic."""
|
|
37
|
+
event_key = "event_{}".format(topic.name)
|
|
38
|
+
self.remove_all_listeners(event_key)
|
|
39
|
+
|
|
40
|
+
def advertise(self, topic):
|
|
41
|
+
"""Announce this code will publish messages to the specified topic.
|
|
42
|
+
|
|
43
|
+
This call has no effect on the in-memory transport."""
|
|
44
|
+
advertise_id = "advertise:{}:{}".format(topic.name, self.id_counter)
|
|
45
|
+
# in-memory does not need anything here
|
|
46
|
+
return advertise_id
|
|
47
|
+
|
|
48
|
+
def unadvertise(self, topic):
|
|
49
|
+
"""Announce this code will stop publishing messages to the specified topic.
|
|
50
|
+
|
|
51
|
+
This call has no effect on the in-memory transport."""
|
|
52
|
+
pass
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# fmt: off
|
|
2
|
+
from __future__ import print_function
|
|
3
|
+
|
|
4
|
+
from ..core import MessageEncoder
|
|
5
|
+
from ..core import Transport
|
|
6
|
+
from ..event_emitter import EventEmitterMixin
|
|
7
|
+
|
|
8
|
+
import clr
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
lib_dir = os.path.join(os.path.dirname(__file__), "netlib")
|
|
14
|
+
if lib_dir not in sys.path:
|
|
15
|
+
sys.path.append(lib_dir)
|
|
16
|
+
|
|
17
|
+
clr.AddReference("MQTTnet")
|
|
18
|
+
|
|
19
|
+
from System import Action
|
|
20
|
+
from System.Text import Encoding
|
|
21
|
+
from System.Threading import CancellationToken, CancellationTokenSource, ManualResetEventSlim
|
|
22
|
+
from System.Threading.Tasks import Task
|
|
23
|
+
|
|
24
|
+
from MQTTnet import MqttFactory
|
|
25
|
+
from MQTTnet import MqttApplicationMessageBuilder
|
|
26
|
+
from MQTTnet.Client import MqttClientConnectResult
|
|
27
|
+
from MQTTnet.Client import MqttClientConnectResultCode
|
|
28
|
+
from MQTTnet.Client import MqttClientOptionsBuilder
|
|
29
|
+
from MQTTnet.Client import MqttClientDisconnectOptionsBuilder
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MqttTransport(Transport, EventEmitterMixin):
|
|
33
|
+
def __init__(self, host, port=1883, *args, **kwargs):
|
|
34
|
+
super(MqttTransport, self).__init__(*args, **kwargs)
|
|
35
|
+
self.host = host
|
|
36
|
+
self.port = port
|
|
37
|
+
self._is_connected = False
|
|
38
|
+
self.cancellation_token_source = CancellationTokenSource()
|
|
39
|
+
self.cancellation_token = self.cancellation_token_source.Token
|
|
40
|
+
|
|
41
|
+
self.factory = MqttFactory()
|
|
42
|
+
options = MqttClientOptionsBuilder().WithTcpServer(host, port).Build()
|
|
43
|
+
|
|
44
|
+
self.client = self.factory.CreateMqttClient()
|
|
45
|
+
self.client.ConnectAsync(options, self.cancellation_token).ContinueWith.Overloads[Action[Task[MqttClientConnectResult]]](self._on_connect)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _on_connect(self, event_args):
|
|
49
|
+
if event_args.Result.ResultCode == MqttClientConnectResultCode.Success:
|
|
50
|
+
self._is_connected = event_args.Result.ResultCode == MqttClientConnectResultCode.Success
|
|
51
|
+
self.emit("ready")
|
|
52
|
+
|
|
53
|
+
def close(self):
|
|
54
|
+
options = MqttClientDisconnectOptionsBuilder().WithReasonString("Normal").Build()
|
|
55
|
+
self.client.DisconnectAsync(options, CancellationToken.None) # noqa: E999 (disable flake8 error, which incorrectly parses None as the python keyword)
|
|
56
|
+
|
|
57
|
+
def on_ready(self, callback):
|
|
58
|
+
if self._is_connected:
|
|
59
|
+
callback()
|
|
60
|
+
else:
|
|
61
|
+
self.once("ready", callback)
|
|
62
|
+
|
|
63
|
+
def publish(self, topic, message):
|
|
64
|
+
# event_key = "event_{}".format(topic.name)
|
|
65
|
+
|
|
66
|
+
# TODO: can we avoid the additional cast to dict?
|
|
67
|
+
application_message = (
|
|
68
|
+
MqttApplicationMessageBuilder()
|
|
69
|
+
.WithTopic(topic.name)
|
|
70
|
+
.WithPayload(json.dumps(dict(message), cls=MessageEncoder))
|
|
71
|
+
.Build()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _callback(**kwargs):
|
|
75
|
+
self.client.PublishAsync(application_message, CancellationToken.None)
|
|
76
|
+
# self.emit(event_key, message)
|
|
77
|
+
|
|
78
|
+
self.on_ready(_callback)
|
|
79
|
+
|
|
80
|
+
def subscribe(self, topic, callback):
|
|
81
|
+
event_key = "event_{}".format(topic.name)
|
|
82
|
+
|
|
83
|
+
def on_message(event_args):
|
|
84
|
+
payload = Encoding.UTF8.GetString(event_args.ApplicationMessage.Payload)
|
|
85
|
+
msg = topic.message_type.parse(json.loads(payload))
|
|
86
|
+
callback(msg)
|
|
87
|
+
|
|
88
|
+
def _subscribe_callback(**kwargs):
|
|
89
|
+
self.on(event_key, callback)
|
|
90
|
+
|
|
91
|
+
subscribe_opts = self.factory.CreateSubscribeOptionsBuilder().WithTopicFilter(lambda f: f.WithTopic(topic.name)).Build()
|
|
92
|
+
self.client.ApplicationMessageReceivedAsync += on_message
|
|
93
|
+
self.client.SubscribeAsync(subscribe_opts, self.cancellation_token)
|
|
94
|
+
|
|
95
|
+
self.on_ready(_subscribe_callback)
|
|
96
|
+
|
|
97
|
+
def advertise(self, topic):
|
|
98
|
+
advertise_id = "advertise:{}:{}".format(topic.name, self.id_counter)
|
|
99
|
+
# mqtt does not need anything here
|
|
100
|
+
return advertise_id
|
|
101
|
+
|
|
102
|
+
def unadvertise(self, topic):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def unsubscribe(self, topic):
|
|
106
|
+
# self.client.unsubscribe(topic.name)
|
|
107
|
+
pass
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import paho.mqtt.client as mqtt
|
|
4
|
+
|
|
5
|
+
from ..core import MessageEncoder
|
|
6
|
+
from ..core import Transport
|
|
7
|
+
from ..event_emitter import EventEmitterMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MqttTransport(Transport, EventEmitterMixin):
|
|
11
|
+
def __init__(self, host, port=1883, *args, **kwargs):
|
|
12
|
+
super(MqttTransport, self).__init__(*args, **kwargs)
|
|
13
|
+
self.host = host
|
|
14
|
+
self.port = port
|
|
15
|
+
self._is_connected = False
|
|
16
|
+
self.client = mqtt.Client() # todo: generate client_id
|
|
17
|
+
self.client.on_connect = self._on_connect
|
|
18
|
+
self.client.connect(self.host, self.port)
|
|
19
|
+
self.client.loop_start()
|
|
20
|
+
|
|
21
|
+
def close(self):
|
|
22
|
+
self.client.loop_stop()
|
|
23
|
+
|
|
24
|
+
def _on_connect(self, client, userdata, flags, rc):
|
|
25
|
+
self._is_connected = True
|
|
26
|
+
self.emit("ready")
|
|
27
|
+
|
|
28
|
+
def on_ready(self, callback):
|
|
29
|
+
if self._is_connected:
|
|
30
|
+
callback()
|
|
31
|
+
else:
|
|
32
|
+
self.once("ready", callback)
|
|
33
|
+
|
|
34
|
+
def publish(self, topic, message):
|
|
35
|
+
# event_key = "event_{}".format(topic.name)
|
|
36
|
+
|
|
37
|
+
def _callback(**kwargs):
|
|
38
|
+
# TODO: can we avoid the additional cast to dict?
|
|
39
|
+
json_message = json.dumps(dict(message), cls=MessageEncoder)
|
|
40
|
+
self.client.publish(topic.name, json_message)
|
|
41
|
+
# self.emit(event_key, message)
|
|
42
|
+
|
|
43
|
+
self.on_ready(_callback)
|
|
44
|
+
|
|
45
|
+
def subscribe(self, topic, callback):
|
|
46
|
+
event_key = "event_{}".format(topic.name)
|
|
47
|
+
|
|
48
|
+
def _subscribe_callback(**kwargs):
|
|
49
|
+
self.client.subscribe(topic.name)
|
|
50
|
+
|
|
51
|
+
def on_message(client, userdata, msg):
|
|
52
|
+
msg = topic.message_type.parse(json.loads(msg.payload.decode()))
|
|
53
|
+
callback(msg)
|
|
54
|
+
|
|
55
|
+
self.client.on_message = on_message
|
|
56
|
+
|
|
57
|
+
self.on(event_key, callback)
|
|
58
|
+
|
|
59
|
+
self.on_ready(_subscribe_callback)
|
|
60
|
+
|
|
61
|
+
def advertise(self, topic):
|
|
62
|
+
advertise_id = "advertise:{}:{}".format(topic.name, self.id_counter)
|
|
63
|
+
# mqtt does not need anything here
|
|
64
|
+
return advertise_id
|
|
65
|
+
|
|
66
|
+
def unadvertise(self, topic):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def unsubscribe(self, topic):
|
|
70
|
+
self.client.unsubscribe(topic.name)
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
from __future__ import division
|
|
3
|
+
from __future__ import print_function
|
|
4
|
+
|
|
5
|
+
import compas
|
|
6
|
+
import compas.plugins
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@compas.plugins.plugin(category="install")
|
|
10
|
+
def installable_rhino_packages():
|
|
11
|
+
return ["compas_eve"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: compas-eve
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: COMPAS Event Extensions: adds event-based communication infrastructure to the COMPAS framework.
|
|
5
|
+
Home-page: https://github.com/compas-dev/compas_eve
|
|
6
|
+
Author: Gonzalo Casas
|
|
7
|
+
Author-email: casas@arch.ethz.ch
|
|
8
|
+
License: MIT license
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: Unix
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
License-File: AUTHORS.md
|
|
26
|
+
|
|
27
|
+
# compas_eve
|
|
28
|
+
|
|
29
|
+
COMPAS Event Extensions: adds event-based communication infrastructure to the COMPAS framework.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Getting started with this project
|
|
33
|
+
|
|
34
|
+
### Setup code editor
|
|
35
|
+
|
|
36
|
+
1. Open project folder in VS Code
|
|
37
|
+
2. Select python environment for the project
|
|
38
|
+
3. First time using VS Code and on Windows? Make sure select the correct terminal profile: `Ctrl+Shift+P`, `Terminal: Select Default Profile` and select `Command Prompt`.
|
|
39
|
+
|
|
40
|
+
> All terminal commands in the following sections can be run from the VS Code integrated terminal.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### First steps with git
|
|
44
|
+
|
|
45
|
+
1. Go to the `Source control` tab
|
|
46
|
+
2. Make an initial commit with all newly created files
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### First steps with code
|
|
50
|
+
|
|
51
|
+
1. Install the newly created project
|
|
52
|
+
|
|
53
|
+
pip install -e .
|
|
54
|
+
|
|
55
|
+
2. Install it on Rhino
|
|
56
|
+
|
|
57
|
+
python -m compas_rhino.install
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Code conventions
|
|
61
|
+
|
|
62
|
+
Code convention follows [PEP8](https://pep8.org/) style guidelines and line length of 120 characters.
|
|
63
|
+
|
|
64
|
+
1. Check adherence to style guidelines
|
|
65
|
+
|
|
66
|
+
invoke lint
|
|
67
|
+
|
|
68
|
+
2. Format code automatically
|
|
69
|
+
|
|
70
|
+
invoke format
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
### Documentation
|
|
74
|
+
|
|
75
|
+
Documentation is generated automatically out of docstrings and [RST](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) files in this repository
|
|
76
|
+
|
|
77
|
+
1. Generate the docs
|
|
78
|
+
|
|
79
|
+
invoke docs
|
|
80
|
+
|
|
81
|
+
2. Check links in docs are valid
|
|
82
|
+
|
|
83
|
+
invoke linkcheck
|
|
84
|
+
|
|
85
|
+
3. Open docs in your browser (file explorer -> `dist/docs/index.html`)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
### Testing
|
|
89
|
+
|
|
90
|
+
Tests are written using the [pytest](https://docs.pytest.org/) framework
|
|
91
|
+
|
|
92
|
+
1. Run all tests from terminal
|
|
93
|
+
|
|
94
|
+
invoke test
|
|
95
|
+
|
|
96
|
+
2. Or run them from VS Code from the `Testing` tab
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### Developing Grasshopper components
|
|
100
|
+
|
|
101
|
+
We use [Grasshopper Componentizer](https://github.com/compas-dev/compas-actions.ghpython_components) to develop Python components that can be stored and edited on git.
|
|
102
|
+
|
|
103
|
+
1. Build components
|
|
104
|
+
|
|
105
|
+
invoke build-ghuser-components
|
|
106
|
+
|
|
107
|
+
2. Install components on Rhino
|
|
108
|
+
|
|
109
|
+
python -m compas_rhino.install
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
### Publish release
|
|
113
|
+
|
|
114
|
+
Releases follow the [semver](https://semver.org/spec/v2.0.0.html) versioning convention.
|
|
115
|
+
|
|
116
|
+
1. Create a new release
|
|
117
|
+
|
|
118
|
+
invoke release major
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
AUTHORS.md
|
|
2
|
+
CHANGELOG.md
|
|
3
|
+
LICENSE
|
|
4
|
+
MANIFEST.in
|
|
5
|
+
README.md
|
|
6
|
+
pyproject.toml
|
|
7
|
+
requirements.txt
|
|
8
|
+
setup.cfg
|
|
9
|
+
setup.py
|
|
10
|
+
src/compas_eve/__init__.py
|
|
11
|
+
src/compas_eve/__main__.py
|
|
12
|
+
src/compas_eve/core.py
|
|
13
|
+
src/compas_eve/event_emitter.py
|
|
14
|
+
src/compas_eve.egg-info/PKG-INFO
|
|
15
|
+
src/compas_eve.egg-info/SOURCES.txt
|
|
16
|
+
src/compas_eve.egg-info/dependency_links.txt
|
|
17
|
+
src/compas_eve.egg-info/not-zip-safe
|
|
18
|
+
src/compas_eve.egg-info/requires.txt
|
|
19
|
+
src/compas_eve.egg-info/top_level.txt
|
|
20
|
+
src/compas_eve/memory/__init__.py
|
|
21
|
+
src/compas_eve/mqtt/__init__.py
|
|
22
|
+
src/compas_eve/mqtt/mqtt_cli.py
|
|
23
|
+
src/compas_eve/mqtt/mqtt_paho.py
|
|
24
|
+
src/compas_eve/mqtt/netlib/MQTTnet.dll
|
|
25
|
+
src/compas_eve/rhino/__init__.py
|
|
26
|
+
src/compas_eve/rhino/install.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
compas_eve
|