diff --git a/docs/_static/principle.svg b/docs/_static/principle.svg index c6df3945b257c25498c864f5d03d3b65caee61e7..06184c62fe4d3d8326c73e0ab1955327c4fc8be0 100644 --- a/docs/_static/principle.svg +++ b/docs/_static/principle.svg @@ -2,9 +2,9 @@ LocalHEROLocalHERORemoteHERORemoteHEROcapabilitiescapabilitiesdiscoverdiscoverobjectobjectproxyobjectproxyobjectsignallingsignallingRPCRPCinspectinspectconstruct + y="-2.8168223" + id="text12-6" + transform="rotate(45)">construct diff --git a/docs/apiref.rst b/docs/apiref.rst deleted file mode 100644 index 0d0ae254095230a5450a71c8d36838c49da34972..0000000000000000000000000000000000000000 --- a/docs/apiref.rst +++ /dev/null @@ -1,25 +0,0 @@ -API Reference -############# - -This is reference for the API of HEROS. - -.. autoclass:: heros.HEROPeer - -.. autoclass:: heros.HERO - -.. autoclass:: heros.LocalHERO - - -.. autoclass:: heros.RemoteHERO - - -.. autoclass:: heros.HEROObserver - - -.. autoclass:: heros.LocalDatasourceHERO - - -.. autoclass:: heros.PolledLocalDatasourceHERO - - -.. autoclass:: heros.DatasourceObserver \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 82e9f35d12fd7ac845ea6dad668a65885910f79d..0457eb59a82ce7cc51eea0a7a5b8e6a0c31f99d1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,10 +13,15 @@ author = 'Thomas Niederprüm' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration +# -- General configuration --------------------------------------------------- extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.napoleon", + # 'sphinx.ext.autodoc', # Include docstrings in the documentation + 'autoapi.extension', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', # Support for Google and NumPy style docstrings + 'sphinx.ext.viewcode', # Add links to source code + 'sphinx.ext.todo', # Enable todo lists + 'sphinx_autodoc_typehints', # Handle type hints in documentation ] templates_path = ['_templates'] @@ -34,3 +39,13 @@ html_theme_options = { "dark_logo": "heros_logo.svg", "sidebar_hide_name": False, } + +# Autodoc settings +autoclass_content = "both" +# -- AutoAPI configuration --------------------------------------------------- +autoapi_type = 'python' +autoapi_dirs = ['../src'] # Path to your source code +autoapi_add_toctree_entry = True # Avoid duplicate toctree entries +autoapi_keep_files = False # Keep intermediate reStructuredText files +# todo conf +todo_include_todos = True diff --git a/docs/events.rst b/docs/events.rst new file mode 100644 index 0000000000000000000000000000000000000000..e1bfd3e81e060fd2510a2c150a3e2caf0aa0f9b1 --- /dev/null +++ b/docs/events.rst @@ -0,0 +1,104 @@ +Events and Callbacks +#################### + +.. _sec-events: + +Events +------ +Methods of custom classes inheriting from ``LocalHERO`` can be marked as an event using the ``@event`` decorator. +Doing so replaces the method with a ``heros.event.LocalEventHandler`` and implies two things: + + 1. The method now supports callbacks (see :ref:`sec-callbacks`) which are executed using the return value of the decorated method. + 2. The return value of the decorated method is published to a unique endpoint within the realm of the zenoh network. + +In the remote representation of the object - the ``RemoteHERO`` - the event is represented as a ``heros.event.RemoteEventHandler``. +This the remote event supports callbacks which are executed using the payload received via the local event endpoint in the zenoh network. + +.. note:: + The remote representation of an event is not callable itself. + + +.. _sec-callbacks: + +Callbacks +--------- +The syntax for using callbacks is regardless wether the we deal with a local or a remote event. +Connecting or disconnecting a callable ``func`` as a callback to an event ``my_event`` is performed via ``my_event.connect(func)`` and ``my_event.disconnect(func)``, respectively. +Calling ``my_event.get_callbacks()`` returns a list of callbacks. + +Callbacks of events can be categorized into four cases differentiated by the execution context of the event (``LocalEventHandler`` or ``RemoteEventHandler``) and the type of the callable. +Consider the event ``my_event`` of ``alice``: + +.. code-block:: python + + class Alice(LocalHERO): + @event + def my_event(self, value): + return do_sth(value) + + # instantiate a local alice + alice = Alice() + +Additionally, consider the three execution contexts (i.e. three python interpreters on three hosts): + + 1. ``ALICE`` where the ``LocalHERO`` of a class ``Alice`` is instantiated. + 2. ``BOB`` where the ``LocalHERO`` of a class ``Bob`` is instantiated. + 3. ``EVE`` where only remote representations (``RemoteHERO``) of ``alice`` and ``bob`` are present. + +For a full example, also consider ``examples/connect_event_callbacks.py``. + + +Case A: Local object, local callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + # execution context ALICE + alice.my_event.connect(print) + +Whenever ``my_event`` returns, the function ``print`` is called in the execution context ``ALICE``. +Therefore, the callback can be understood as ``print(alice.my_event(value))``. + + +Case B: Local object, remote callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + # execution context ALICE + # get a remote representation of bob + remote_bob = RemoteHERO("bob") + alice.my_event.connect(remote_bob.the_builder) + +Whenever ``my_event`` returns, the method ``the_builder`` of the remote representation of ``bob`` is called in the execution context ``ALICE``. +Therefore, the callback can be understood as ``remote_bob.the_builder(alice.my_event(value))``. + + +Case C: Remote object, local callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + # execution context EVE + # get a remote representation of alice + remote_alice = RemoteHERO("alice") + remote_alice.my_event.connect(print) + +Whenever ``my_event`` of ``alice`` (the ``LocalHERO``) returns, the function ``print`` is called in the execution context of ``EVE``. +In this case, the ``RemoteEventHandler`` representation of the ``my_event`` (``remote_alice.my_event``) calls ``print`` once it receives the return value of ``alice.my_event`` via the zenoh network. + + +Case D: Remote object, remote callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + # execution context EVE + # get remote representations of alice and bob + remote_alice = RemoteHERO("alice") + remote_bob = RemoteHERO("bob") + remote_alice.my_event.connect(remote_bob.the_builder) + +Connecting the remote callable ``remote_bob.the_builder`` to the remote representation ``remote_alice.my_event`` in the context of ``EVE`` leads to a special behavior. +The callable ``remote_bob.the_builder`` is automatically attached as a remote callback to ``alice`` (the ``LocalHERO``) similar to case B. +This way, calling ``remote_bob.the_builder`` upon return of ``alice.my_event`` is handled as a direct P2P connection between the contexts ``ALICE`` and ``BOB``. diff --git a/docs/index.rst b/docs/index.rst index e5fe232b943faa6bdf93270ca2d24d1501a6fe8d..f949c51007ca360e4202fbf868ee120083a75875 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,18 +7,15 @@ HEROS - Highly Efficient Remote Object Service =============================================== HEROS is a decentralized object sharing service. In simple words it makes your software objects network transparent. -To be fast and efficient, HEROS relies on eclipse-zenoh as a transport layer. It thus supports different network -topologies and hardware transports. Most notably, it can run completely decentralized, avoiding a single point -of failure and at the same time guaranteeing low latency and and high bandwidth communication through p2p connections. +To be fast and efficient, HEROS relies on eclipse-zenoh as a transport layer. +It thus supports different network topologies and hardware transports. +Most notably, it can run completely decentralized, avoiding a single point of failure and at the same time guaranteeing low latency and and high bandwidth communication through p2p connections. -HEROS provides a logical representation of software objects and is not tied to any specific language. Even non-object -oriented programming languages might provide a collection of functions, variables, and events to be accessible as an -object in HEROS. +HEROS provides a logical representation of software objects and is not tied to any specific language. +Even non-object oriented programming languages might provide a collection of functions, variables, and events to be accessible as an object in HEROS. -Very much like a Dynamic Invocation Infrastructure (DII) in a Common Object Broker Architecture (CORBA), HEROS handles -objects dynamically during runtime rather than during compile time. While this does not allow to map HEROS objects to -be mapped to the language objects in compiled languages, languages supporting monkey-patching (python, js, ...) are -still able to create proxy objects during runtime. +Very much like a Dynamic Invocation Infrastructure (DII) in a Common Object Broker Architecture (CORBA), HEROS handles objects dynamically during runtime rather than during compile time. +While this does not allow to map HEROS objects to be mapped to the language objects in compiled languages, languages supporting monkey-patching (python, js, ...) are still able to create proxy objects during runtime. The working principle is shown in the following. @@ -34,5 +31,4 @@ The working principle is shown in the following. paradigms installation usage - apiref - + events diff --git a/docs/paradigms.rst b/docs/paradigms.rst index ad508528cdedebee87eada329896dd04dbe9ce37..8427859085d6685c2833b28e2c427e8c96de754e 100644 --- a/docs/paradigms.rst +++ b/docs/paradigms.rst @@ -4,21 +4,20 @@ Paradigms Realms ------ -To isolate groups of HERO objects from other groups the concept of realms exists in HEROS. You can think of it as a -namespace where objects in the same namespace can talk to each other while communication across realms/namespaces is -not easily possible. Note that this is solely a management feature, not a security feature. All realms share the same -zenoh network and can thus talk to each other on this level. +To isolate groups of HERO objects from other groups the concept of realms exists in HEROS. +You can think of it as a namespace where objects in the same namespace can talk to each other while communication across realms/namespaces is +not easily possible. +Note that this is solely a management feature, not a security feature. +All realms share the same zenoh network and can thus talk to each other on this level. Objects ------- -An object that should be shared via HEROS must inherit from the class `LocalHero`. When python instantiates such -an object, it will parse the methods, class attributes, and events (see event decorator) and automatically generate -a list of capabilities that describes this HEROS object. The capabilities are announced and a liveliness token for the -object is created. HEROSOberserver in the network will thus be notified that our new object joined the realm. +An object that should be shared via HEROS must inherit from the class ``LocalHero``. +When python instantiates such an object, it will parse the methods, class attributes, and :doc:`events ` (see event decorator) and automatically generate a list of capabilities that describes this HEROS object. +The capabilities are announced and a liveliness token for the object is created. ``HEROSOberserver`` in the network will thus be notified that our new object joined the realm. -When the object is destroyed or the link gets lost, the liveliness token disappears and any remote object will notice -this. +When the object is destroyed or the link gets lost, the liveliness token disappears and any remote object will notice this. Capabilities ------------ @@ -33,12 +32,11 @@ A HEROS object is characterized by the capabilities it provides. There are curre Datasource ---------- -A frequent use case for HERO is that of making data (like sensor data or status data) available to interested peers. To cover this use -case, a special class of HERO exists, the DatasourceHERO. It provides a special event `new_data` that is always emitted when new data -is available. RemoteHEROs connect to the emitting HERO will get noticed directly and react accordingly. In addition also a DatasourceObsever class -exist, that efficiently monitors the events of many HEROs in the network without fully instanciating the RemoteHEROs. +A frequent use case for HERO is that of making data (like sensor data or status data) available to interested peers. +To cover this use case, a special class of HERO exists, the ``DatasourceHERO``. It provides a special event ``new_data`` that is always emitted when new data is available. RemoteHEROs connect to the emitting HERO will get noticed directly and react accordingly. +In addition also a ``DatasourceObsever`` class exist, that efficiently monitors the events of many HEROs in the network without fully instantiating the RemoteHEROs. Class Tree ---------- -This is not yet implemented. I will be a method to \ No newline at end of file +This is not yet implemented. diff --git a/docs/usage.rst b/docs/usage.rst index 2e36501fb7c699bd79b62ccbdd4a41cdc17df935..43a7d7e5c3d8b2f778f2b7b2610d19993711856d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,19 +1,17 @@ Usage ##### -Building your first HERO is very simple. Just write your custom class and make it inherit from :code:`heros.LocalHero` +Building your first HERO is very simple. Just write your custom class and make it inherit from ``heros.LocalHero``. A simple example can be seen .. literalinclude:: ../examples/share_local_hero_simple.py :language: python -Due to the infinite loop, the script will not terminate and keep the object `obj` alive. Since it inherits from `heros.LocalHERO`, -the object was analyzed upon instantiation and provided as a HERO to the network. This means, we can now simply access the method -attributes of the object from a remote site. +Due to the infinite loop, the script will not terminate and keep the object ``obj`` alive. Since it inherits from ``heros.LocalHERO``, the object was analyzed upon instantiation and provided as a HERO to the network. +This means, we can now simply access the method attributes of the object from a remote site. -To get remote access we can run the following in a different process or on a different machine in the same network (UDP broadcast -needs to reach the other machine for discovery): +To get remote access we can run the following in a different process or on a different machine in the same network (UDP broadcast needs to reach the other machine for discovery): .. literalinclude:: ../examples/access_remote_hero_simple.py :language: python @@ -21,8 +19,10 @@ needs to reach the other machine for discovery): Nested HEROs ------------ -HEROS is able to serialize HEROs as references to a HERO. This allows to pass a HERO from between the local and the remote site. -When either of the sites receives such a reference, it creates a RemoteHERO to access the referenced HERO. This allows things like +HEROS is able to serialize HEROs as references to a HERO. +This allows to pass a HERO from between the local and the remote site. +When either of the sites receives such a reference, it creates a ``RemoteHERO`` to access the referenced HERO. +This allows things like 1. Deep access to HEROs nested inside of HEROs. 2. Passing HEROs as arguments into methods exposed by a HERO. @@ -32,12 +32,12 @@ When either of the sites receives such a reference, it creates a RemoteHERO to a Unserializable Objects ---------------------- -A HERO attribute or method might return an object that is not a HERO and can not be serialized. In that case, the returned object is -cached on the side of the LocalHERO and an identifier is sent to the remote side. The remote side can store the reference locally. -If the reference is sent back to the LocalHERO side, the corresponding object is taken from the cache and inserted into the request -instead of the reference. This allows to instruct the LocalHERO to do something with an object that cannot be transferred. This -allows, for example, to hand over an unserializable object retrieved earlier as argument to a function. +A HERO attribute or method might return an object that is not a HERO and can not be serialized. +In that case, the returned object is cached on the side of the ``LocalHERO`` and an identifier is sent to the remote side. The remote side can store the reference locally. +If the reference is sent back to the ``LocalHERO`` side, the corresponding object is taken from the cache and inserted into the request instead of the reference. +This allows to instruct the LocalHERO to do something with an object that cannot be transferred. +This allows, for example, to hand over an unserializable object retrieved earlier as argument to a function. .. note:: - The cache that keeps the object references uses only weak references to to avoid memory leaks. That means that an object can be garbage - collected if not any other instance keeps a reference on it. \ No newline at end of file + The cache that keeps the object references uses only weak references to to avoid memory leaks. + That means that an object can be garbage collected if not any other instance keeps a reference on it. diff --git a/examples/connect_event_callbacks.py b/examples/connect_event_callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..435c055a65201ea552c76e5d0c9fe97a331a71e2 --- /dev/null +++ b/examples/connect_event_callbacks.py @@ -0,0 +1,76 @@ +from heros import LocalHERO, event, RemoteHERO, log + +import asyncio + +log.setLevel("DEBUG") + + +class Alice(LocalHERO): + + testme: int = 0 + + def print_from_afar(self, payload)-> str: + print(f"printed from {self._name} {payload}") + + def hello(self) -> str: + self.testme += 1 + return "world" + + @event + def new_data(self, value): + return value + + +class Bob(Alice): + pass + + +# Create two local objects and two remote representations of them. +alice = Alice("alice") +bob = Bob("bob") + +remote_alice = RemoteHERO("alice") +remote_bob = RemoteHERO("bob") + +# When connecting these objects with events and callbacks, we can distinguish 4 cases: +alice.new_data.connect(print) # case A: local object, local callback +alice.new_data.connect(remote_bob.print_from_afar) # case B: local object, remote callback + +remote_alice.new_data.connect(print) # case C: remote object, local callback +remote_alice.new_data.connect(remote_bob.print_from_afar) # case D: remote object, remote callback (p2p dispatch) + +# Print the connected callbacks for all objects +for o in [alice, remote_alice]: + print(f"Connected callbacks for {o}:") + print(o.new_data.get_callbacks()) + print("###") + +# Now remove the all callbacks from remote_alice which are called from remote_alice +for cb in remote_alice.new_data.get_callbacks(): + if cb["context"] == "RemoteHERO": + print(f"Removing {cb['name']} from remote_alice.new_data") + remote_alice.new_data.disconnect(cb["func"]) +print("###") +print(f"Remaining callbacks to remote_alice.new_data") +print(remote_alice.new_data.get_callbacks()) +print("###") + + +# Emit data from the from events +loop = asyncio.new_event_loop() +async def emit_data(): + i = 0 + while True: + alice.new_data(["Alice", i]) + bob.new_data(["Bob", i]) + i += 1 + await asyncio.sleep(1) + +# Keep running +loop.create_task(emit_data()) +try: + loop.run_forever() +except KeyboardInterrupt: + pass +finally: + obj.close() diff --git a/pyproject.toml b/pyproject.toml index 8311aa8b2121f137671baf38dbc0805258860616..b5556bc719fdff29b2cbd2aa9fde0cc8ff8a74ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,16 +4,18 @@ build-backend = "hatchling.build" [project] name = "heros" -version = "0.3.0" +version = "0.4.0" description = "Make your objects available everywhere" readme = "README.md" license = {text = "LGPL-3.0-or-later"} keywords = ["remote object", "zenoh", "pub/sub", "rpc"] authors = [ - {name = "Thomas Niederprüm", email = "t.niederpruem@rptu.de"} + {name = "Thomas Niederprüm", email = "t.niederpruem@rptu.de"}, + {name = "Suthep Pomjaksilp", email = "s.pomjaksilp@rptu.de"}, ] maintainers = [ - {name = "Thomas Niederprüm", email = "t.niederpruem@rptu.de"} + {name = "Thomas Niederprüm", email = "t.niederpruem@rptu.de"}, + {name = "Suthep Pomjaksilp", email = "s.pomjaksilp@rptu.de"}, ] requires-python = ">=3.9" @@ -48,5 +50,9 @@ Documentation = "https://gitlab.com/atomiq-project/heros/wiki" [project.optional-dependencies] dev = [ "pytest>=7.1", - "flake8>=6.0" + "flake8>=6.0", + "sphinx>=8", + "sphinx-autoapi>=3", + "sphinx-autodoc-typehints>=3", + "furo>=2024", ] diff --git a/src/heros/capabilities.py b/src/heros/capabilities.py index 679a386dd1e8389696805aed1340c0edfe490b3f..ca95044a3742c03c61d8a235d51ea07d9e4b1e06 100644 --- a/src/heros/capabilities.py +++ b/src/heros/capabilities.py @@ -88,8 +88,8 @@ class Capability: @dataclass class AttributeCapability(Capability): """ - An attribute capability describes a single variable of the remote object that is exposed - under the name of the capability. + An attribute capability describes a single variable of the remote object. + It is exposed under the name of the capability. Args: name: name of the capability @@ -100,13 +100,13 @@ class AttributeCapability(Capability): type: str access: str = "rw" - def to_dict(self): + def to_dict(self) -> dict: d = Capability.to_dict(self) d.update({"type": self.type, "access": self.access}) return d @staticmethod - def from_dict(d: dict): + def from_dict(d: dict) -> "AttributeCapability": if "name" not in d: raise AttributeError("required field 'type' not in dict") return AttributeCapability(name=d["name"], type=d["type"], access=d["access"]) @@ -116,14 +116,11 @@ class AttributeCapability(Capability): class EventCapability(Capability): """ An event capability describes the ability of a remote object to notify upon a certain event. - - Args: - """ flavor: ClassVar[str] = "event" @staticmethod - def from_dict(d: dict): + def from_dict(d: dict) -> "EventCapability": return EventCapability(name=d["name"]) @@ -134,7 +131,7 @@ class MethodCapability(Capability): return_type: str = "None" @staticmethod - def from_method(m: Callable): + def from_method(m: Callable) -> "MethodCapability": sig = inspect.signature(m) cap = MethodCapability(name=m.__name__) @@ -161,7 +158,7 @@ class MethodCapability(Capability): return d @staticmethod - def from_dict(d: dict): + def from_dict(d: dict) -> "MethodCapability": """ Generate a method capabilities object from a defining dictionary. diff --git a/src/heros/datasource/datasource.py b/src/heros/datasource/datasource.py index e36b96b9d2679fb7d8f98fe4a2bcc608e67e2632..bce8641ab8bb8879afa5313bf52c7c2b37e6759b 100644 --- a/src/heros/datasource/datasource.py +++ b/src/heros/datasource/datasource.py @@ -9,12 +9,13 @@ from .observables import ObservableProcessor class LocalDatasourceHERO(LocalHERO): """ - A datasource is a HERO that can provide information on a standardized interface. This interface is the event - `new_data`. Instances in the zenoh network interested in the data provided by data sources can simply subscribe - to the key expression `@objects/realm/*/new_data` or use the :class:RemoteDatasourceObserver(). + A datasource is a HERO that can provide information on a standardized interface. + This interface is the event `new_data`. Instances in the zenoh network interested in the data provided + by data sources can simply subscribe to the key expression `@objects/realm/*/new_data` or use + the :class:`DatasourceObserver`. To make your class a LocalDatasourceHERO make it inherit this class. - This class is ment for datasources that create asynchronous events on their own. When processing such an event + This class is meant for datasources that create asynchronous events on their own. When processing such an event call `new_data` to publish the data from this datasource. Args: @@ -36,11 +37,12 @@ class LocalDatasourceHERO(LocalHERO): class DatasourceObserver(HEROPeer): """ - A class that can observe and handle the data emitted by one or more datasource HEROs. In particular, this - class provides an efficient way to listen to the data emitted by all datasource HEROs in the realm. By - not instantiating the HEROs themselves but just subscribing to the topics for the datasource, this reduces the - pressure on the backing zenoh network. If, however, only the data of a few HEROs should be observed, it might - make more sense to just instantiate the according RemoteHEROs and connect a callback to their `new_data` signal. + A class that can observe and handle the data emitted by one or more datasource HEROs. + In particular, this class provides an efficient way to listen to the data emitted by all datasource HEROs in + the realm. By not instantiating the HEROs themselves but just subscribing to the topics for the datasource, this + reduces the pressure on the backing zenoh network. If, however, only the data of a few HEROs should be observed, + it might make more sense to just instantiate the according RemoteHEROs and connect a callback to their `new_data` + signal. Args: object_selector: selector to specify which objects to observe. This becomes part of a zenoh selector and thus @@ -87,8 +89,9 @@ class PolledLocalDatasourceHERO(LocalDatasourceHERO): To make your class a PolledLocalDatasourceHERO make it inherit this class an implement the method `_new_data`. The method will get called periodically and the return value will be published as an event. - Note: The periodic calling is realized via asyncio and will thus only work if the asyncio mainloop is - started. + Note: + The periodic calling is realized via asyncio and will thus only work if the asyncio mainloop is + started. Args: name: name/identifier under which the object is available. Make sure this name is unique in the realm. diff --git a/src/heros/datasource/observables.py b/src/heros/datasource/observables.py index 2cab505928d7c38ab7e500bbf65be24135ee7ef3..d4800222b33c172a83d2dbb79203fd4ef89af513 100644 --- a/src/heros/datasource/observables.py +++ b/src/heros/datasource/observables.py @@ -4,8 +4,9 @@ from typing import Optional def lambdify(expression: str, globals_dict: dict = {}): """ - Turn a string into a lambda function with the given globals. By default it enables a set of numpy functions - in the expression. Further functions can be added by providing it in :param:`globals_dict`. + Turn a string into a lambda function with the given globals. + By default it enables a set of numpy functions in the expression. Further functions can be added by providing + it in ``globals_dict``. """ import numpy as np np_functions = ["sin", "cos", "tan", "arcsin", "arccos", "arctan", "hypot", "arctan2", "sinh", "cosh", "tanh", @@ -22,8 +23,7 @@ def lambdify(expression: str, globals_dict: dict = {}): class BoundaryChecker: """ Check whether the observed value is in it's boundaries. - Therefore provide an integer feedback with rising importance. - where boundary = 0 says that everything is fine. + Therefore provide an integer feedback with rising importance where boundary = 0 says that everything is fine. 1, 2, 3 correspond to higher warning / error or fault situations. """ @@ -80,7 +80,7 @@ class Observable: class ObservableProcessor: """ - An ObservableProcessor takes a configuration dict for multiple observables when it is instanciated. + An ObservableProcessor takes a configuration dict for multiple observables when it is instantiated. An object of this class can be called with a :class:DataSourceReturnSet as an argument. """ def __init__(self, obs_def: dict): diff --git a/src/heros/datasource/types.py b/src/heros/datasource/types.py index b689c28bc6b4feb40767c60b2ebaca690b437828..fb43287a69c913243ff22e3530e651aa6fed1707 100644 --- a/src/heros/datasource/types.py +++ b/src/heros/datasource/types.py @@ -7,10 +7,11 @@ def ensure_string(x): class DatasourceReturnValue(dict): """ - A structure to store data returned from a single entity in a datasource. A datasource can return multiple - entities at once. In this case many DatasourceReturnValues are stored in a :class:DatasourceReturnSet. + A structure to store data returned from a single entity in a datasource. + A datasource can return multiple entities at once. In this case many DatasourceReturnValues are stored in + a :class:`DatasourceReturnSet`. - Default return values from datasource. They can be converted using a calibration. + Default return values from datasource. They can be converted using a calibration. :param raw_value: (float) :param raw_unit: (str[10]) :param time: (int) creation time of the rawValue. @@ -92,13 +93,13 @@ class DatasourceReturnValue(dict): class DatasourceReturnSet(tuple): """ - collection of multiple DatasourceReturnValues + Collection of multiple :class:`DatasourceReturnValue`. """ @staticmethod def from_data(data): """ - we try to build a DatasourceReturnSet by guessing the data format from the following options: + We try to build a DatasourceReturnSet by guessing the data format from the following options: * [FLOAT, FLOAT, ..] -> A list of raw_values * [(FLOAT, STR), (FLOAT, STR), ..] -> a list of (raw_value, raw_unit) tuples * {STR: FLOAT, STR: FLOAT, ..} -> a dict with id: raw_value diff --git a/src/heros/event.py b/src/heros/event.py index 4358b5d49229b6742bbbc20ee0c1c069ca2e9ef5..60849329b20f27ebd7931be16857d61793bcf97f 100644 --- a/src/heros/event.py +++ b/src/heros/event.py @@ -1,38 +1,398 @@ -from .inspect import mark_hero_event, is_hero_event +import functools -def event(func: callable): +from .helper import log +from .inspect import mark_hero_event, is_hero_event, is_hero_method, HERO_EVENT_ATTRIBUTE + + +def remote_hero_method_to_str(func: callable) -> str: + return f"{func.__self__._name}.{func.__name__}" + + +class Callback: """ - decorator for events. Only use on mehtods bound to objects since this assumes - that the first argument passed to the decorated function is `self`. + Represent a callback function. """ - def _inner(*args, **kwargs): - result = func(*args, **kwargs) - # extract object - self = args[0] + def __init__(self, func: callable, origin: str = None): + self.func = func + self.origin = origin + if is_hero_method(func): + self.name = remote_hero_method_to_str(func) + self.is_remote_hero = True + else: + self.name = repr(func) + self.is_remote_hero = False + + def __eq__(self, other) -> bool: + """ + Check for equality with other `Callback`. + + Args: + other: other callback instance. + + Returns: + bool: equality result. + """ + if self.is_remote_hero == other.is_remote_hero: + if self.is_remote_hero: + # both True + # check if names are equal + return self.name == other.name + # both False + # check if callback functions are equal + return self.func == other.func + # one is remote and the other one is not + return False + + def __call__(self, *args, **kwargs): + """ + Call the callback function. + """ + return self.func(*args, **kwargs) + + def __hash__(self): + """ + Generate a hash value for this callback using the name in case of remote hero methods + or the callable itself for builtins or local callables. + + Returns: + int: calculated hash. + """ + if self.is_remote_hero: + return hash(self.name) + else: + return hash(self.func) + + def to_dict(self) -> dict: + """ + Generate a dictionary representation of this callback. + + Returns: + dict: dictionary with keys: name, origin, is_remote_hero and func + """ + return {"name": self.name, "origin": self.origin, "is_remote_hero": self.is_remote_hero, "func": self.func} + + +class CallbackStorage: + """ + Store all callbacks. + """ + + def __init__(self): + self._callbacks = {} + + def __iter__(self): + """ + Generate an iteration for this iterable. + """ + return iter(self._callbacks.values()) + + def __contains__(self, func: callable) -> bool: + """ + Implements the `in` operation for this class. + """ + callback = Callback(func) + return callback.name in self._callbacks.keys() + + def append(self, func: callable, origin: str = None) -> str: + """ + Append a given callable to the storage. + + Args: + func: `callable` to append. + origin: `str` (default: `None`) indicating the origin of the callback. + + Returns: + str: name of the callback. + """ + callback = Callback(func, origin) + self._callbacks[callback.name] = callback + return callback.name - # publish result - self._session.put(f"{self._endpoint_base_path}/{func.__name__}", self._serialize(result)) + def remove(self, func: callable) -> bool: + """ + Remove a callable from storage. - # mark the method as event - _inner = mark_hero_event(_inner) + Args: + func: `callable` to remove. + + Returns: + bool: truth value indicating if the callable was a callback. + """ + if func in self: + callback = Callback(func) + del self._callbacks[callback.name] + return True + return False + + def is_callback(self, func: callable) -> bool: + """ + Check if given `callable` is a callback. - return _inner + Args: + func: `callable` to check. + + Returns: + bool: `callable` is a callback + """ + return func in self + def get_callbacks(self) -> list: + """ + Get a list of all callbacks dictionaries. -class RemoteEvent: - def __init__(self, name): - self.name = name - self.callbacks = [] + Returns: + list: dictionary representation of all callbacks + """ + return [cb.to_dict() for name, cb in self._callbacks.items()] + + +class EventHandler: + """ + Base class for event handlers. + """ def connect(self, callback: callable): - if callback not in self.callbacks: - self.callbacks.append(callback) + """Connect a callback function.""" + raise NotImplementedError def disconnect(self, callback: callable): - if callback in self.callbacks: - del self.callbacks[self.callbacks.index(callback)] + """Disconnect a callback function.""" + raise NotImplementedError + + def is_callback(self, func: callable) -> bool: + """Check if `func` is a callback""" + raise NotImplementedError + + def get_callbacks(self): + """Return all callbacks""" + raise NotImplementedError + + +class LocalEventHandler(EventHandler): + """ + Handles event connections for a specific instance. + """ + + def __init__(self, instance, func): + self.instance = instance + self.func = func + # store callbacks for this instance + self.callbacks = CallbackStorage() + # mark as an hero event + setattr(self, HERO_EVENT_ATTRIBUTE, True) + # preserve signature and metadata of `func` + functools.update_wrapper(self, func) - def trigger(self, payload): + def __call__(self, *args, **kwargs): + """ + Call the original function and trigger callbacks. + """ + # call the method + result = self.func(self.instance, *args, **kwargs) + # publish result (triggers RemoteEvent callbacks) + endpoint = f"{self.instance._endpoint_base_path}/{self.func.__name__}" + self.instance._session.put(endpoint, self.instance._serialize(result)) + # execute callbacks + for callback in self.callbacks: + callback(result) + return result + + def connect(self, callback: callable, origin: str = None) -> str: + """ + Connect a callback function to be triggered when the method is called. + + Args: + callback: `callable` to connect. + origin: (optional) `str` indicting origin of this callback. + + Returns: + str: name of the callback. + """ + callback_name = self.callbacks.append(callback, origin) + log.debug(f"{self.__class__} connecting LocalEvent callback {self.__name__} -> {callback_name} (origin: {origin})") + return callback_name + + def disconnect(self, callback: callable) -> bool: + """ + Disconnect a callback function. + + Args: + callback: `callable` to disconnect. + + Returns: + bool: truth value indicating if the callable was a callback. + """ + log.debug(f"{self.__class__} disconnecting LocalEvent callback {self.__name__} -> {callback}") + return self.callbacks.remove(callback) + + def is_callback(self, func: callable) -> bool: + """ + Check if given callable is already a registered callback. + + Args: + callback: `callable` to check. + + Returns: + bool: truth value indicating if the callable is a callback. + """ + if is_hero_method(func): + return remote_hero_method_to_str(func) in self.remote_hero_callbacks.keys() + else: + return func in self.callbacks + + def get_callbacks(self) -> list: + """ + Return a list of registered callback functions. + + Returns: + list: dictionary representation of all callbacks + """ + return [{**cb, "context": "LocalHERO"} for cb in self.callbacks.get_callbacks()] + + +class RemoteEventHandler(EventHandler): + """ + Handles remote events for a specific instance. + """ + + def __init__(self, instance, func: callable = None): + self.instance = instance + # store callbacks for this instance + self.callbacks = CallbackStorage() + # mark as an hero event + setattr(self, HERO_EVENT_ATTRIBUTE, True) + + def __call__(self, payload): + """ + Call the original function and trigger callbacks. + """ for callback in self.callbacks: callback(payload) + + def connect(self, callback: callable) -> str: + """ + Connect a callback function to be triggered when the method is called. + + Args: + callback: `callable` to connect. + origin: (optional) `str` indicting origin of this callback. + + Returns: + str: name of the callback. + """ + if is_hero_method(callback): + callback_name = self.instance._connect_local_hero_callback( + event=self, + remote_hero_method=callback, + origin=self.instance._name + ) + log.debug(f"{self.__class__} connecting LocalEvent callback {self.__name__} -> {callback}") + elif callback not in self.callbacks: + callback_name = self.callbacks.append(callback) + log.debug(f"{self.__class__} connecting RemoteEvent callback {self.__name__} -> {callback}") + return callback_name + + def disconnect(self, callback: callable) -> None: + """ + Disconnect a callback function. + + Args: + callback: `callable` to disconnect. + + Returns: + bool: truth value indicating if the callable was a callback. + """ + if is_hero_method(callback): + log.debug(f"{self.__class__} disconnecting LocalEvent callback {self.__name__} -> {callback}") + return self.instance._disconnect_local_hero_callback(event=self, remote_hero_method=callback) + if callback in self.callbacks: + log.debug(f"{self.__class__} disconnecting RemoteEvent callback {self.__name__} -> {callback}") + return self.callbacks.remove(callback) + + def is_callback(self, func: callable) -> bool: + """ + Check if given callable is already a registered callback. + + Args: + callback: `callable` to check. + + Returns: + bool: truth value indicating if the callable is a callback. + """ + return func in self.callbacks + + def get_callbacks(self) -> list: + """ + Return a list of registered callback functions. + + Returns: + list: dictionary representation of all callbacks + """ + remote_event_cbs = [{**cb, "context": "RemoteHERO"} for cb in self.callbacks.get_callbacks()] + local_event_cbs = [{**cb, "context": "LocalHERO"} for cb in self.instance._get_local_hero_callbacks(event=self)] + return [*remote_event_cbs, *local_event_cbs] + + +class EventDescriptor: + """ + A descriptor to handle instance-specific event connections. + """ + + def __init__(self, func: callable = None): + # store the original function + self.func = func + # store callbacks for each instance + self._instances = {} + + @staticmethod + def _get_event_handler_cls(): + raise NotImplementedError + + def __get__(self, instance, owner): + """ + Ensure the method and event-handling functions are bound to the instance. + + Args: + self: the EventDescriptor instance + instance: the owning `LocalHERO`/`RemoteHERO`. + owner: + """ + if instance is None: + # return descriptor itself if accessed via the class + return self + # create an event handler for this instance if not already created + if instance not in self._instances: + self._instances[instance] = self._get_event_handler_cls()(instance, self.func) + # return the instance-bound event handler + return self._instances[instance] + + +class LocalEventDescriptor(EventDescriptor): + """ + Descriptor of `@event` decorated methods of a `LocalHERO`. + """ + + @staticmethod + def _get_event_handler_cls(): + return LocalEventHandler + + +class RemoteEventDescriptor(EventDescriptor): + """ + Descriptor of remote representations of events in a `RemoteHERO`. + """ + + @staticmethod + def _get_event_handler_cls(): + return RemoteEventHandler + + +def event(func: callable): + """ + Decorator for events. + + Note: + Only use on methods bound to objects. + """ + return LocalEventDescriptor(func) diff --git a/src/heros/helper.py b/src/heros/helper.py index 46c1f7fd8cb41b6b3d3debd1a33ef16c2abdefb1..2a95bf91e086068d124e44ea4ddddbed02e398b6 100644 --- a/src/heros/helper.py +++ b/src/heros/helper.py @@ -10,7 +10,8 @@ def object_name_from_keyexpr(key_expr, ns_objects, realm, endpoint=".*"): def full_classname(o): - """ Return the fully qualified class name of an object + """ + Return the fully qualified class name of an object. Args: o: object diff --git a/src/heros/heros.py b/src/heros/heros.py index c4f208c66abc0fc146be5b02b13a6df9d555f79a..a33859bcc88d266f0f1704f4412e62fa5d98769d 100644 --- a/src/heros/heros.py +++ b/src/heros/heros.py @@ -2,8 +2,11 @@ import zenoh import traceback from functools import partial from .capabilities import Capability, MethodCapability, AttributeCapability, EventCapability -from .event import RemoteEvent -from .inspect import is_hero_event, is_local_only, local_only +from .event import RemoteEventDescriptor +from .inspect import ( + is_hero_event, is_local_only, is_force_remote, is_hero_method, + local_only, force_remote, mark_hero_method +) from heros import serdes from heros.zenoh import session_manager as default_session_manager from .helper import object_name_from_keyexpr, log @@ -11,10 +14,10 @@ from .helper import object_name_from_keyexpr, log class HEROPeer: """ - A HEROPeer provides the minimal interface to establish the HERO communication on top of the zenoh backend. To this - end, it provides methods to send cbor-serialized messages via the zenoh network. It establishes the `@object` - namespace and communicates in a defined realm. Methods to discover objects in the realm and to retrieve their - object information are provided. + A HEROPeer provides the minimal interface to establish the HERO communication on top of the zenoh backend. + To this end, it provides methods to send cbor-serialized messages via the zenoh network. It establishes + the `@object` namespace and communicates in a defined realm. Methods to discover objects in the realm and + to retrieve their object information are provided. Args: realm: Name of the realm that this HEROPeer belongs to. (default: heros) @@ -63,8 +66,8 @@ class HEROPeer: def _subscribe_selector(self, selector: str, callback: callable, *args, **kwargs): """ - Subscribe to a zenoh selector and a attach a callback that receives the deserialized payload of the messages - published. + Subscribe to a zenoh selector and a attach a callback. + The callback receives the deserialized payload of the messages published. Args: selector: zenoh selector for the subscription. See the zenoh documentation for valid descriptors. @@ -96,8 +99,8 @@ class HEROPeer: def _discover(self, timeout: float = 10.0) -> dict: """ - Send query to discovery endpoint of all HEROs in the current realm. All alive objects will respond and - send their remote object descriptor + Send query to discovery endpoint of all HEROs in the current realm. + All alive objects will respond and send their remote object descriptor. Args: timeout: timeout for the discover operation in seconds (default: 10) @@ -155,8 +158,8 @@ class HERO(HEROPeer): def _query_endpoint(self, endpoint: str, *args, **kwargs) -> list: """ - Send a query to an endpoint. This is a wrapper for _query_selector that enforces to talk to the endpoint - of the remote object. + Send a query to an endpoint. + This is a wrapper for _query_selector that enforces to talk to the endpoint of the remote object. Args: endpoint: endpoint within this HERO. If the given endpoint does not start with the endpoint base path, @@ -171,8 +174,8 @@ class HERO(HEROPeer): def _subscribe_endpoint(self, endpoint: str, callback: callable, *args, **kwargs): """ - Subscribe to an endpoint of this HERO. This is a wrapper for _subscribe_selector that enforces to talk to the - endpoint of the remote object. + Subscribe to an endpoint of this HERO. + This is a wrapper for _subscribe_selector that enforces to talk to the endpoint of the remote object. Args: endpoint: endpoint within this HERO. If the given endpoint does not start with the endpoint base path, @@ -188,8 +191,8 @@ class HERO(HEROPeer): class RemoteHERO(HERO): """ - Creates a local stub object from a remote HERO such that it seems like the remote object is a local - object. The remote HERO is identified by its name and has to be available at the given realm. + Creates a local stub object from a remote HERO such that it seems like the remote object is a local object. + The remote HERO is identified by its name and has to be available at the given realm. Attribute and method capabilities of the remote object are directly mapped to attributes and methods of the stub object, respectively. The signature of the methods is adapted accordingly. The remote attributes do not @@ -200,7 +203,8 @@ class RemoteHERO(HERO): To be able to attach attributes to this class, every instance of a `RemoteHERO` is created from a dynamically generated child class of `RemoteHERO` with the name `RemoteHERO__`. - Note: To discover which objects are available in a certain realm, see :class:HEROObserver. + Note: + To discover which objects are available in a certain realm, see :class:HEROObserver. Args: name: name/identifier of the remote object @@ -249,7 +253,7 @@ class RemoteHERO(HERO): return self._query_endpoint(_cap.name, payload=_cap.call_dict(*args, **kwargs)) # attach stub function to self - setattr(self, cap.name, f) + setattr(self, cap.name, mark_hero_method(f)) getattr(self, cap.name).__signature__ = cap.to_signature() getattr(self, cap.name).__name__ = cap.name getattr(self, cap.name).__self__ = self @@ -267,20 +271,24 @@ class RemoteHERO(HERO): setattr(self.__class__, cap.name, property(partial(f_getter, cap), partial(f_setter, cap))) elif isinstance(cap, EventCapability): - remote_event = RemoteEvent(name=cap.name) - self._subscribe_endpoint(cap.name, remote_event.trigger) + remote_event = RemoteEventDescriptor() setattr(self.__class__, cap.name, remote_event) + getattr(self, cap.name).__name__ = cap.name + self._subscribe_endpoint(cap.name, getattr(self, cap.name)) # calls __call__ def __eq__(self, other): return self.__class__.__name__ == other.__class__.__name__ and \ self._realm == other._realm and \ self._name == other._name + def __hash__(self): + return hash((self.__class__.__name__, self._realm, self._name)) + class LocalHERO(HERO): """ - Base class for objects exposed through HEROS. Any object that should be able to be accessed remotely must be based - off this class. + Base class for objects exposed through HEROS. + Any object that should be able to be accessed remotely must be based off this class. Args: name: name/identifier under which the object is available. Make sure this name is unique in the realm. @@ -323,15 +331,18 @@ class LocalHERO(HERO): self.capabilities = [] for member_name in dir(self.__class__): # skip members we do not want to expose - if (member_name.startswith("_") or member_name in dir(LocalHERO) or - is_local_only(getattr(self.__class__, member_name))): + exclude = any([ + member_name.startswith("_"), + member_name in dir(LocalHERO), + is_local_only(getattr(self.__class__, member_name)) + ]) + if exclude and (not is_force_remote(getattr(self.__class__, member_name))): continue if callable(getattr(self, member_name)): if is_hero_event(getattr(self, member_name)): - print(f"found event with name {member_name}!") - + log.debug(f"found event with name {member_name}!") cap = EventCapability(name=member_name) wrapper = None else: @@ -375,11 +386,54 @@ class LocalHERO(HERO): self._session.declare_queryable(f"{self._endpoint_base_path}/{member_name}", partial(wrapper, cap)) self.capabilities.append(cap) + @force_remote + def _connect_local_hero_callback(self, event: callable , remote_hero_method: callable, origin: str = None) -> str: + """ + Connect a method of `RemoteHERO` as a callback to an event of the `LocalHERO`. + This leads to a new, direct P2P connection between the `RemoteHERO` and the `LocalHERO` to call the method. + + Args: + event: the event `callable`, i.e. a method that is decorated with `@event` in the `LocalHERO`. + remote_hero_method: `callable` to connect as a callback. + origin: optional `str` indicating the semantic origin of the connection. + + Returns: + str: name of the callback. + """ + return getattr(self, event.__name__).connect(remote_hero_method, origin) + + @force_remote + def _disconnect_local_hero_callback(self, event: callable , remote_hero_method: callable) -> bool: + """ + Disconnect a method of `RemoteHERO` from an event of the `LocalHERO`. + + Args: + event: the event `callable`, i.e. a method that is decorated with `@event` in the `LocalHERO`. + remote_hero_method: `callable` to connect as a callback. + + Returns: + bool: truth value if the remote method was indeed a callback. + """ + return getattr(self, event.__name__).disconnect(remote_hero_method) + + @force_remote + def _get_local_hero_callbacks(self, event: callable) -> list: + """ + Get a list of dictionary representations of the callbacks of an event of the `LocalHERO`. + + Args: + event: the event `callable`, i.e. a method that is decorated with `@event` in the `LocalHERO`. + + Returns: + list: dictionary representations of the callbacks. + """ + return getattr(self, event.__name__).get_callbacks() + class HEROObserver(HEROPeer): """ A HEROObserver keeps track of the HEROs in a given realm by monitoring its zenoh liveliness tokens. - The member attribute `known_objects` always holds a list of all HEROs known to the observer. + The member attribute ``known_objects`` always holds a list of all HEROs known to the observer. Args: realm: Name of the realm that this HEROPeer belongs to. (default: heros) diff --git a/src/heros/inspect.py b/src/heros/inspect.py index 307e91d0853933a3f6092e2b0b76280e360086cd..76e90be4f36801d74f488c476dfafebc5688675d 100644 --- a/src/heros/inspect.py +++ b/src/heros/inspect.py @@ -1,6 +1,7 @@ HERO_EVENT_ATTRIBUTE = "hero_event" HERO_METHOD_ATTRIBUTE = "hero_method" LOCAL_ONLY_ATTRIBUTE = "local_only" +FORCE_REMOTE_ATTRIBUTE = "force_remote" def _mark(func: callable, marker: str) -> callable: @@ -76,6 +77,21 @@ def mark_local_only(func: callable) -> callable: # rename mark_local_only to use as a decorator local_only = mark_local_only +def mark_force_remote(func: callable) -> callable: + """ + Mark a callable as force remote. + + Args: + func: callable to mark + + Returns: + The marked callable. + """ + return _mark(func, FORCE_REMOTE_ATTRIBUTE) + +# rename mark_force_remote to use as a decorator +force_remote = mark_force_remote + def is_hero_event(func: callable) -> bool: """ @@ -117,3 +133,16 @@ def is_local_only(func: callable) -> bool: `False` if the marker is not present. """ return _is_marked(func, LOCAL_ONLY_ATTRIBUTE) + +def is_force_remote(func: callable) -> bool: + """ + Check if a callable is a force remote. + + Args: + func: callable to check + + Returns: + The value of the marker. + `False` if the marker is not present. + """ + return _is_marked(func, FORCE_REMOTE_ATTRIBUTE) diff --git a/src/heros/serdes.py b/src/heros/serdes.py index 906aad7cc40f2cc64abc803261053d2f909a0676..a2e5ae9bbdc9a532b54c77d0a9e7355c4843c181 100644 --- a/src/heros/serdes.py +++ b/src/heros/serdes.py @@ -3,11 +3,14 @@ import numpy as np import weakref from heros.helper import full_classname +from .inspect import is_hero_event, is_hero_method ndarray_tag = 4242 hero_tag = 9000 unserializable_tag = 9001 unserializable_object_reference_tag = 9002 +hero_event_tag = 9003 +hero_method_tag = 9004 def is_builtin_class_instance(obj): @@ -16,8 +19,7 @@ def is_builtin_class_instance(obj): class ObjectStore: """ - An object store that can be used to keep week references to objects identified by a serializable - identifier (int64) + An object store to be used to keep week references to objects identified by a serializable identifier (int64). """ _cache = {} @@ -37,8 +39,8 @@ class ObjectStore: def get(self, identifier: int) -> object: """ - Retrieve an object identified by :param:identifier from the store. If the object does not - exist any more, None is returned. + Retrieve an object identified by :param:identifier from the store. + If the object does not exist any more, None is returned. Args: identifier: the identifier obtained when storing the object @@ -75,7 +77,8 @@ class UnserializableRemoteObject: def cbor_default_encoder(encoder, value): - """Handle custom types in serialization + """ + Handle custom types in serialization. """ from heros import LocalHERO, RemoteHERO global obj_store @@ -88,6 +91,16 @@ def cbor_default_encoder(encoder, value): # encode a HERO encoder.encode(cbor2.CBORTag(hero_tag, [value._realm, value._name])) + elif is_hero_event(value): + # encode an event of a remote hero + # value.instance corresponds is the HERO instance + encoder.encode(cbor2.CBORTag(hero_event_tag, [value.instance, value.__name__])) + + elif is_hero_method(value): + # encode a method of a remote hero + # value.__self__ corresponds is the HERO instance + encoder.encode(cbor2.CBORTag(hero_method_tag, [value.__self__, value.__name__])) + elif type(value) is UnserializableRemoteObject: # encode an reference on an remote object that cannot be serialized encoder.encode(cbor2.CBORTag(unserializable_object_reference_tag, value.id)) @@ -112,6 +125,16 @@ def cbor_tag_hook(decoder, tag, shareable_index=None): realm, name = tag.value return RemoteHERO(name, realm=realm) + if tag.tag == hero_event_tag: + # decode a remote HERO event + remote_hero, event_name = tag.value + return getattr(remote_hero, event_name) + + if tag.tag == hero_method_tag: + # decode a remote HERO method + remote_hero, method_name = tag.value + return getattr(remote_hero, method_name) + if tag.tag == unserializable_tag: # decode t, i, s = tag.value diff --git a/src/heros/zenoh.py b/src/heros/zenoh.py index 7d0e90b689d8886b40f604b1485aa2a73f7794b3..2e8934a7368d10c6759e3f435c0dad795e81af76 100644 --- a/src/heros/zenoh.py +++ b/src/heros/zenoh.py @@ -11,7 +11,7 @@ class ZenohSessionManager: def request_session(self, obj: object) -> zenoh.Session: """ - Request the global zenoh session + Request the global zenoh session. Args: obj: The object that requests the session @@ -31,7 +31,7 @@ class ZenohSessionManager: def release_session(self, obj: object): """ - Release from the global zenoh session + Release from the global zenoh session. Args: obj: The object that wants to release from the global zenoh session