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.
@@ -0,0 +1,3 @@
1
+ # Authors
2
+
3
+ - Gonzalo Casas <<casas@arch.ethz.ch>> [@gonzalocasas](https://github.com/gonzalocasas)
@@ -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
+
@@ -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,2 @@
1
+ compas
2
+ paho-mqtt
@@ -0,0 +1,18 @@
1
+ [bdist_wheel]
2
+ universal = 1
3
+
4
+ [flake8]
5
+ max-line-length = 120
6
+ exclude = */migrations/*
7
+
8
+ [doc8]
9
+ max-line-length = 120
10
+ ignore = D001
11
+
12
+ [pydocstyle]
13
+ convention = numpy
14
+
15
+ [egg_info]
16
+ tag_build =
17
+ tag_date = 0
18
+
@@ -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,2 @@
1
+ if __name__ == "__main__":
2
+ pass
@@ -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,8 @@
1
+ import sys
2
+
3
+ if sys.platform == "cli":
4
+ from .mqtt_cli import MqttTransport
5
+ else:
6
+ from .mqtt_paho import MqttTransport
7
+
8
+ __all__ = ["MqttTransport"]
@@ -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)
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,2 @@
1
+ compas
2
+ paho-mqtt
@@ -0,0 +1 @@
1
+ compas_eve