From 3289a65535cb1d8bf065e3ec5137ea5e863425c2 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sun, 21 Jul 2024 12:56:53 +0200 Subject: [PATCH 01/36] release workflow for test pypi before merging to release branch --- .github/workflows/{test.yml => test-dev.yml} | 0 .github/workflows/test-release.yml | 32 ++++++++++++++++++++ 2 files changed, 32 insertions(+) rename .github/workflows/{test.yml => test-dev.yml} (100%) create mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test-dev.yml similarity index 100% rename from .github/workflows/test.yml rename to .github/workflows/test-dev.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 0000000..237c69c --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,32 @@ +name: Unit Tests With TestPyPI + +on: + workflow_dispatch: + pull_request: + branches: + - release + + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: 3.11 + + - name: Install dependencies + run: | + pip install jsonschema + pip install -i https://test.pypi.org/simple/ hololinked + + - name: Run unit tests to verify if the release to TestPyPI is working + run: | + python -m unittest discover -s tests -p 'test_*.py' + + + \ No newline at end of file From 4d47b677b4fda93a6ac5742eec2980b15a8a29c5 Mon Sep 17 00:00:00 2001 From: "Vignesh.Vaidyanathan" Date: Mon, 22 Jul 2024 13:26:17 +0200 Subject: [PATCH 02/36] update comment on intro level --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00f2ed3..d94c250 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ one has to provide these three interactions with the hardware. In this package, can instantiate properties, actions and events which become visible to a client in this segragated manner. For example, consider an optical spectrometer, the following code is possible: -> This is a fairly mid-level intro, if you are beginner, for another variant check [How-To](https://hololinked.readthedocs.io/en/latest/howto/index.html) +> This is a fairly mid-level intro focussed on HTTP. If you are beginner or looking for ZMQ, for another variant check [How-To](https://hololinked.readthedocs.io/en/latest/howto/index.html) #### Import Statements From 0a141f04e3d4c1ed4c4cbf4e39016fd64d105426 Mon Sep 17 00:00:00 2001 From: "Vignesh.Vaidyanathan" Date: Mon, 22 Jul 2024 13:47:16 +0200 Subject: [PATCH 03/36] edit project title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d94c250..43dc639 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# hololinked - Pythonic Supervisory Control & Data Acquisition / Internet of Things +# hololinked - Pythonic Object-Oriented Supervisory Control & Data Acquisition / Internet of Things ### Description From d82adbdfd417a5b5d95f4004999a6384b81f5ecf Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:29:43 +0200 Subject: [PATCH 04/36] rename tests --- .github/workflows/test-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index ce5083b..2900858 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -1,4 +1,4 @@ -name: Python Unit Tests +name: Unit Tests For Development on: workflow_dispatch: From 46aff3e85974ae7d1dad6a18a3a9f4e102a96a7d Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:06:57 +0200 Subject: [PATCH 05/36] added discord invite --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43dc639..783d91f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ For beginners - `hololinked` is a server side pythonic package suited for instru

For those familiar with RPC & web development - This package is an implementation of a ZeroMQ-based Object Oriented RPC with customizable HTTP end-points. A dual transport in both ZMQ and HTTP is provided to maximize flexibility in data type, serialization and speed, although HTTP is preferred for networked applications. If one is looking for an object oriented approach towards creating components within a control or data acquisition system, or an IoT device, one may consider this package. -[![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked) +[![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked) [![Discord](https://img.shields.io/discord/1265289049783140464)](https://discord.gg//kEz87zqQXh) ### To Install From 1ef204ada0e72b340bc8ad491a0ca7955abc67b1 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:47:10 +0200 Subject: [PATCH 06/36] add email as badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 783d91f..e8200c8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ For beginners - `hololinked` is a server side pythonic package suited for instru

For those familiar with RPC & web development - This package is an implementation of a ZeroMQ-based Object Oriented RPC with customizable HTTP end-points. A dual transport in both ZMQ and HTTP is provided to maximize flexibility in data type, serialization and speed, although HTTP is preferred for networked applications. If one is looking for an object oriented approach towards creating components within a control or data acquisition system, or an IoT device, one may consider this package. -[![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked) [![Discord](https://img.shields.io/discord/1265289049783140464)](https://discord.gg//kEz87zqQXh) +[![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked) +[![Discord](https://img.shields.io/discord/1265289049783140464)](https://discord.gg//kEz87zqQXh) [![email](https://img.shields.io/badge/email-example)](mailto:vignesh.vaidyanathan@hololinked.dev) ### To Install From 4c3fb3ab3dcb004a3878ee51318550642c4cd98d Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:34:41 +0200 Subject: [PATCH 07/36] edit beginner info --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8200c8..9695243 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ one has to provide these three interactions with the hardware. In this package, can instantiate properties, actions and events which become visible to a client in this segragated manner. For example, consider an optical spectrometer, the following code is possible: -> This is a fairly mid-level intro focussed on HTTP. If you are beginner or looking for ZMQ, for another variant check [How-To](https://hololinked.readthedocs.io/en/latest/howto/index.html) +> This is a fairly mid-level intro focussed on HTTP. If you are beginner or looking for ZMQ which can be used with no networking knowledge, check [How-To](https://hololinked.readthedocs.io/en/latest/howto/index.html) #### Import Statements From 791b9dd58ebe95869ce107b4a557f85f0ebcb5b8 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:50:40 +0200 Subject: [PATCH 08/36] added timeout to locks on client which prevents infinite hang --- hololinked/server/zmq_message_brokers.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/hololinked/server/zmq_message_brokers.py b/hololinked/server/zmq_message_brokers.py index ee6e481..4db7094 100644 --- a/hololinked/server/zmq_message_brokers.py +++ b/hololinked/server/zmq_message_brokers.py @@ -1542,9 +1542,13 @@ def execute(self, instruction : str, arguments : typing.Dict[str, typing.Any] = message id : bytes a byte representation of message id """ + acquire_timeout = -1 if invokation_timeout is None else invokation_timeout + acquired = self._client_queue.acquire(timeout=acquire_timeout) + if not acquired: + raise TimeoutError("previous request still in progress") try: - self._client_queue.acquire() - msg_id = self.send_instruction(instruction, arguments, invokation_timeout, execution_timeout, context, argument_schema) + msg_id = self.send_instruction(instruction, arguments, invokation_timeout, + execution_timeout, context, argument_schema) return self.recv_reply(msg_id, raise_client_side_exception=raise_client_side_exception, deserialize=deserialize_reply) finally: self._client_queue.release() @@ -1742,7 +1746,10 @@ async def async_execute(self, instruction : str, arguments : typing.Dict[str, ty a byte representation of message id """ try: - await self._client_queue.acquire() + await asyncio.wait_for(self._client_queue.acquire(), timeout=invokation_timeout) + except TimeoutError: + raise TimeoutError("previous request still in progress") from None + try: msg_id = await self.async_send_instruction(instruction, arguments, invokation_timeout, execution_timeout, context, argument_schema) return await self.async_recv_reply(msg_id, raise_client_side_exception=raise_client_side_exception, From 7714be4c906703f9a241080f5da08eacf098a86c Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:10:48 +0200 Subject: [PATCH 09/36] example for simplistic cases for action and property --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9695243..f30699b 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ class OceanOpticsSpectrometer(Thing): serial_number = String(default=None, allow_None=True, URL_path='/serial-number', doc="serial number of the spectrometer to connect/or connected", http_method=("GET", "PUT")) - # GET and PUT is default for reading and writing the property respectively. - # Use other HTTP methods if necessary. + # GET and PUT is default for reading and writing the property respectively. + # So you can leave it out, especially if you are going to use ZMQ and dont understand HTTP integration_time = Number(default=1000, bounds=(0.001, None), crop_to_bounds=True, URL_path='/integration-time', @@ -154,6 +154,12 @@ class OceanOpticsSpectrometer(Thing): self.serial_number = serial_number self.device = Spectrometer.from_serial_number(self.serial_number) self._wavelengths = self.device.wavelengths().tolist() + + @action() # So you can leave it out, especially if you are going to use ZMQ and dont understand HTTP + def disconnect(self): + """disconnect from the spectrometer""" + self.device.close() + ``` Methods that are neither decorated with action decorator nor acting as getters-setters of properties remain as plain python methods and are **not** accessible on the network. From 8d61e157f4b749077c8839d0e8f714359dfd7c86 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:11:29 +0200 Subject: [PATCH 10/36] tab edit in action example --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f30699b..cfc3ce6 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,8 @@ class OceanOpticsSpectrometer(Thing): self.device = Spectrometer.from_serial_number(self.serial_number) self._wavelengths = self.device.wavelengths().tolist() - @action() # So you can leave it out, especially if you are going to use ZMQ and dont understand HTTP + # So you can leave it out, especially if you are going to use ZMQ and dont understand HTTP + @action() def disconnect(self): """disconnect from the spectrometer""" self.device.close() From e59ca5c9835bc5fd818a696fe15b0caa383fafff Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:20:25 +0200 Subject: [PATCH 11/36] update control panel client github address --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 320cf06..f1c59e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ Otherwise, I will then take care of the issue as soon as possible. Developers are always welcome to contribute to the code base. If you want to tackle any issues, un-existing features, let me know (at my email), I can create some open issues and features which I was never able to solve or did not have the time. You can also suggest what else can be contributed functionally or conceptually or also simply code-refactoring. The lack of issues or features in the [Issues](https://github.com/VigneshVSV/hololinked/issues) section of github does not mean the project is considered feature complete or I dont have ideas what to do next. On the contrary, there is tons of work to do. There are also repositories which can use your skills: -- An [admin client](https://github.com/VigneshVSV/hololinked-portal) in react +- An [admin client](https://github.com/VigneshVSV/thing-control-panel) in react - [Documentation](https://github.com/VigneshVSV/hololinked-docs) in sphinx which needs significant improvement in How-To's, beginner level docs which may teach people concepts of data acquisition or IoT, Docstring or API documentation of this repository itself - [Examples](https://github.com/VigneshVSV/hololinked-examples) in nodeJS, Dashboard/PyQt GUIs or server implementations using this package. Hardware implementations of unexisting examples are also welcome, I can open a directory where people can search for code based on hardware and just download your code. diff --git a/README.md b/README.md index cfc3ce6..2f17996 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ For beginners - `hololinked` is a server side pythonic package suited for instru For those familiar with RPC & web development - This package is an implementation of a ZeroMQ-based Object Oriented RPC with customizable HTTP end-points. A dual transport in both ZMQ and HTTP is provided to maximize flexibility in data type, serialization and speed, although HTTP is preferred for networked applications. If one is looking for an object oriented approach towards creating components within a control or data acquisition system, or an IoT device, one may consider this package. [![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked) -[![Discord](https://img.shields.io/discord/1265289049783140464)](https://discord.gg//kEz87zqQXh) [![email](https://img.shields.io/badge/email-example)](mailto:vignesh.vaidyanathan@hololinked.dev) +[![email](https://img.shields.io/badge/email-example)](mailto:vignesh.vaidyanathan@hololinked.dev) ### To Install @@ -299,7 +299,7 @@ Here one can see the use of `instance_name` and why it turns up in the URL path. ##### NOTE - The package is under active development. Contributors welcome, please check CONTRIBUTING.md. - [example repository](https://github.com/VigneshVSV/hololinked-examples) - detailed examples for both clients and servers -- [helper GUI](https://github.com/VigneshVSV/hololinked-portal) - view & interact with your object's methods, properties and events. +- [helper GUI](https://github.com/VigneshVSV/thing-control-panel) - view & interact with your object's actions, properties and events. See a list of currently supported possibilities while using this package [below](#currently-supported). From f43fa0bfe7b62a49b3a9598531410642205a7e77 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 27 Jul 2024 11:31:21 +0200 Subject: [PATCH 12/36] add zmq TCP and IPC --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2f17996..bb6ebe4 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,9 @@ if __name__ == '__main__': log_level=logging.DEBUG ) O.run_with_http_server(ssl_context=ssl_context) + # or O.run(zmq_protocols='IPC') - interprocess communication without HTTP + # or O.run(zmq_protocols=['IPC', 'TCP'], tcp_socket_address='tcp://*:9999') + # - both interprocess communication & TCP ``` Here one can see the use of `instance_name` and why it turns up in the URL path. See the detailed example of the above code [here](https://gitlab.com/hololinked-examples/oceanoptics-spectrometer/-/blob/simple/oceanoptics_spectrometer/device.py?ref_type=heads). From 5592ec01b1c6eb160b67f6769457db8d2e3a523e Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sun, 28 Jul 2024 10:57:44 +0200 Subject: [PATCH 13/36] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bb6ebe4..0541112 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ For beginners - `hololinked` is a server side pythonic package suited for instru For those familiar with RPC & web development - This package is an implementation of a ZeroMQ-based Object Oriented RPC with customizable HTTP end-points. A dual transport in both ZMQ and HTTP is provided to maximize flexibility in data type, serialization and speed, although HTTP is preferred for networked applications. If one is looking for an object oriented approach towards creating components within a control or data acquisition system, or an IoT device, one may consider this package. [![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked) -[![email](https://img.shields.io/badge/email-example)](mailto:vignesh.vaidyanathan@hololinked.dev) +
+[![email](https://img.shields.io/badge/email%20me-brown)](mailto:vignesh.vaidyanathan@hololinked.dev) ### To Install From e677fe5289f11760481eb63206e7928a18ac096e Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sun, 28 Jul 2024 23:06:37 +0200 Subject: [PATCH 14/36] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0541112..3746ddf 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ if __name__ == '__main__': Here one can see the use of `instance_name` and why it turns up in the URL path. See the detailed example of the above code [here](https://gitlab.com/hololinked-examples/oceanoptics-spectrometer/-/blob/simple/oceanoptics_spectrometer/device.py?ref_type=heads). -##### NOTE - The package is under active development. Contributors welcome, please check CONTRIBUTING.md. +##### NOTE - The package is under active development. Contributors welcome, please check CONTRIBUTING.md and the open issues. Some issues can also be independently dealt without much knowledge of this package. - [example repository](https://github.com/VigneshVSV/hololinked-examples) - detailed examples for both clients and servers - [helper GUI](https://github.com/VigneshVSV/thing-control-panel) - view & interact with your object's actions, properties and events. From 4eef7f02df789e738dad6ad5ea01c0a2de496deb Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:10:54 +0200 Subject: [PATCH 15/36] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3746ddf..a98fb24 100644 --- a/README.md +++ b/README.md @@ -318,12 +318,12 @@ One may use the HTTP API according to one's beliefs (including letting the packa - auto-generate Thing Description for Web of Things applications. - use serializer of your choice (except for HTTP) - MessagePack, JSON, pickle etc. & extend serialization to suit your requirement. HTTP Server will support only JSON serializer to maintain compatibility with node-wot. Default is JSON serializer based on msgspec. - asyncio compatible - async RPC server event-loop and async HTTP Server - write methods in async -- choose from multiple ZeroMQ transport methods. Some of the possibilities one can achieve by choosing ZMQ transport methods: +- choose from multiple ZeroMQ transport methods which offers some possibilities like the following without changing the code: - run HTTP Server & python object in separate processes or the same process - serve multiple objects with the same HTTP server - run direct ZMQ-TCP server without HTTP details - expose only a dashboard or web page on the network without exposing the hardware itself - + Again, please check examples or the code for explanations. Documentation is being activety improved. ### Currently being worked From 65ec93ad5252620afa254c5b4e346eda3f0f45ef Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:41:30 +0200 Subject: [PATCH 16/36] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a98fb24..2c1b733 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,8 @@ Each device or thing can be controlled systematically when their design in softw - actions are methods which issue commands like connect/disconnect, execute a control routine, start/stop measurement, or run arbitray python logic - events can asynchronously communicate/push (arbitrary) data to a client (say, a GUI), like alarm messages, streaming measured quantities etc. -It does not even matter whether you are controlling your hardware locally or remotely, what protocol you use, what is the nature of the client etc., -one has to provide these three interactions with the hardware. In this package, the base class which enables this classification is the `Thing` class. Any class that inherits the `Thing` class -can instantiate properties, actions and events which -become visible to a client in this segragated manner. For example, consider an optical spectrometer, the following code is possible: +In this package, the base class which enables this classification is the `Thing` class. Any class that inherits the `Thing` class +can instantiate properties, actions and events which become visible to a client in this segragated manner. For example, consider an optical spectrometer, the following code is possible: > This is a fairly mid-level intro focussed on HTTP. If you are beginner or looking for ZMQ which can be used with no networking knowledge, check [How-To](https://hololinked.readthedocs.io/en/latest/howto/index.html) From 54dd28d2305c78839af8eff35470d754ed2b9f0a Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:43:07 +0200 Subject: [PATCH 17/36] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c1b733..796580e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ class OceanOpticsSpectrometer(Thing): In non-expert terms, properties look like class attributes however their data containers are instantiated at object instance level by default. For example, the `integratime_time` property defined above as `Number`, whenever set/written, will be validated as a float or int, cropped to bounds and assigned as an attribute to each instance of the `OceanOpticsSpectrometer` class with an internally generated name. It is not necessary to know this internally generated name as the property value can be accessed again in any python logic, say, `print(self.integration_time)`. -To overload the get-set (or read-write) of properties, one may do the following: +To apply properties directly onto the hardware, one may overload the get-set (or read-write) of properties in the following manner: ```python class OceanOpticsSpectrometer(Thing): From 0beb629bdc3ca87e94da8771978d48711b3369bb Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:44:23 +0200 Subject: [PATCH 18/36] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 796580e..0559b73 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,7 @@ class OceanOpticsSpectrometer(Thing): In non-expert terms, properties look like class attributes however their data containers are instantiated at object instance level by default. For example, the `integratime_time` property defined above as `Number`, whenever set/written, will be validated as a float or int, cropped to bounds and assigned as an attribute to each instance of the `OceanOpticsSpectrometer` class with an internally generated name. It is not necessary to know this internally generated name as the property value can be accessed again in any python logic, say, `print(self.integration_time)`. -To apply properties directly onto the hardware, one may overload the get-set (or read-write) of properties in the following manner: - +To overload the get-set (or read-write) of properties, one may do the following: ```python class OceanOpticsSpectrometer(Thing): From 4e897dcac6f848ef5dac3f5686c92f987c045f98 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:48:44 +0200 Subject: [PATCH 19/36] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0559b73..dcc0705 100644 --- a/README.md +++ b/README.md @@ -290,9 +290,9 @@ if __name__ == '__main__': log_level=logging.DEBUG ) O.run_with_http_server(ssl_context=ssl_context) - # or O.run(zmq_protocols='IPC') - interprocess communication without HTTP + # or O.run(zmq_protocols='IPC') - interprocess communication and no HTTP # or O.run(zmq_protocols=['IPC', 'TCP'], tcp_socket_address='tcp://*:9999') - # - both interprocess communication & TCP + # both interprocess communication & TCP, no HTTP ``` Here one can see the use of `instance_name` and why it turns up in the URL path. See the detailed example of the above code [here](https://gitlab.com/hololinked-examples/oceanoptics-spectrometer/-/blob/simple/oceanoptics_spectrometer/device.py?ref_type=heads). From 94616b551baaed9e9c762196550a1520033ba7bd Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:26:18 +0200 Subject: [PATCH 20/36] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcc0705..b3a2602 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ class OceanOpticsSpectrometer(Thing): ``` In non-expert terms, properties look like class attributes however their data containers are instantiated at object instance level by default. -For example, the `integratime_time` property defined above as `Number`, whenever set/written, will be validated as a float or int, cropped to bounds and assigned as an attribute to each instance of the `OceanOpticsSpectrometer` class with an internally generated name. It is not necessary to know this internally generated name as the property value can be accessed again in any python logic, say, `print(self.integration_time)`. +For example, the `integration_time` property defined above as `Number`, whenever set/written, will be validated as a float or int, cropped to bounds and assigned as an attribute to each instance of the `OceanOpticsSpectrometer` class with an internally generated name. It is not necessary to know this internally generated name as the property value can be accessed again in any python logic, say, `print(self.integration_time)`. To overload the get-set (or read-write) of properties, one may do the following: ```python From 904c3097b754fd3b48b1a3eae96579ff7ffa0eae Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:16:14 +0200 Subject: [PATCH 21/36] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b3a2602..4f1063d 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,8 @@ what the event represents and how to subscribe to it) with subprotocol SSE (HTTP ``` > data schema ("data" field above which describes the event payload) are optional and discussed later +Events follow a pub-sub model with '1 publisher to N subscribers' technique per `Event` object, both through ZMQ and HTTP SSE. + Although the code is the very familiar & age-old RPC server style, one can directly specify HTTP methods and URL path for each property, action and event. A configurable HTTP Server is already available (from `hololinked.server.HTTPServer`) which redirects HTTP requests to the object according to the specified HTTP API on the properties, actions and events. To plug in a HTTP server: ```python From 3239458a5bb0319d81338f8661669544587befb2 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:45:19 +0200 Subject: [PATCH 22/36] fixed type definitions for events which was not turning up in VS code --- hololinked/server/events.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hololinked/server/events.py b/hololinked/server/events.py index 7c73c53..fafe01e 100644 --- a/hololinked/server/events.py +++ b/hololinked/server/events.py @@ -11,6 +11,7 @@ from .security_definitions import BaseSecurityDefinition + class Event: """ Asynchronously push arbitrary messages to clients. Apart from default events created by the package (like state @@ -54,7 +55,11 @@ def __set_name__(self, owner : ParameterizedMetaclass, name : str) -> None: self._obj_name = name self.owner = owner - def __get__(self, obj : ParameterizedMetaclass, objtype : typing.Optional[type] = None) -> "EventDispatcher": + @typing.overload + def __get__(self, obj, objtype) -> "EventDispatcher": + ... + + def __get__(self, obj : ParameterizedMetaclass, objtype : typing.Optional[type] = None): try: return obj.__dict__[self._internal_name] except KeyError: @@ -74,6 +79,7 @@ def __set__(self, obj : Parameterized, value : typing.Any) -> None: else: raise TypeError(f"Supply EventDispatcher object to event {self._obj_name}, not type {type(value)}.") + class EventDispatcher: """ @@ -123,6 +129,8 @@ def push(self, data : typing.Any = None, *, serialize : bool = True, **kwargs) - http_clients=kwargs.get('http_clients', True), serialize=serialize) + + class CriticalEvent(Event): """ Push events to client and get acknowledgement for that From f877d6efaf26577f32837276689571af08403820 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:41:35 +0200 Subject: [PATCH 23/36] compatibility for the thing control panel viewer --- hololinked/server/dataklasses.py | 107 ++++++------------------------- hololinked/server/eventloop.py | 3 +- hololinked/server/logger.py | 14 ++-- hololinked/server/property.py | 7 +- hololinked/server/td.py | 11 ++-- hololinked/server/thing.py | 31 +++++++-- 6 files changed, 60 insertions(+), 113 deletions(-) diff --git a/hololinked/server/dataklasses.py b/hololinked/server/dataklasses.py index aca9107..cf0f018 100644 --- a/hololinked/server/dataklasses.py +++ b/hololinked/server/dataklasses.py @@ -394,99 +394,28 @@ class ServerSentEvent(SerializableDataclass): socket_address : str = field(default=UNSPECIFIED) what : str = field(default=ResourceTypes.EVENT) - -@dataclass -class GUIResources(SerializableDataclass): +def build_our_temp_TD(instance): """ - Encapsulation of all information required to populate hololinked-portal GUI for a thing. - - Attributes - ---------- - instance_name : str - instance name of the ``Thing`` - inheritance : List[str] - inheritance tree of the ``Thing`` - classdoc : str - class docstring - properties : nested JSON (dictionary) - defined properties and their metadata - actions : nested JSON (dictionary) - defined actions - events : nested JSON (dictionary) - defined events - documentation : Dict[str, str] - documentation files, name as key and path as value - GUI : nested JSON (dictionary) - generated from ``hololinked.webdashboard.ReactApp``, a GUI can be shown under 'default GUI' tab in the portal + A temporary extension of TD used to build GUI of thing control panel. + Will be later replaced by a more sophisticated TD builder which is compliant to the actual spec & its theory. """ - instance_name : str - inheritance : typing.List[str] - classdoc : typing.Optional[typing.List[str]] - properties : typing.Dict[str, typing.Any] = field(default_factory=dict) - actions : typing.Dict[str, typing.Any] = field(default_factory=dict) - events : typing.Dict[str, typing.Any] = field(default_factory=dict) - documentation : typing.Optional[typing.Dict[str, typing.Any]] = field(default=None) - GUI : typing.Optional[typing.Dict] = field(default=None) - - def __init__(self): - """ - initialize first, then call build action. - """ - super(SerializableDataclass, self).__init__() - - def build(self, instance): - from .thing import Thing - from .events import Event - - assert isinstance(instance, Thing), f"got invalid type {type(instance)}" - - self.instance_name = instance.instance_name - self.inheritance = [class_.__name__ for class_ in instance.__class__.mro()] - self.classdoc = instance.__class__.__doc__.splitlines() if instance.__class__.__doc__ is not None else None - self.GUI = instance.GUI - - self.events = { - event._unique_identifier.decode() : dict( - name = event._remote_info.name, - instruction = event._unique_identifier.decode(), - owner = event._owner_inst.__class__.__name__, - owner_instance_name = event._owner_inst.instance_name, - address = instance.event_publisher.socket_address - ) for event in instance.event_publisher.events - - } - self.actions = dict() - self.properties = dict() - - for instruction, remote_info in instance.instance_resources.items(): - if remote_info.isaction: - try: - self.actions[instruction] = instance.zmq_resources[instruction].json() - self.actions[instruction]["remote_info"] = instance.httpserver_resources[instruction].json() - self.actions[instruction]["remote_info"]["http_method"] = instance.httpserver_resources[instruction].instructions.supported_methods() - # to check - apparently the recursive json() calling does not reach inner depths of a dict, - # therefore we call json ourselves - self.actions[instruction]["owner"] = instance.zmq_resources[instruction].qualname.split('.')[0] - self.actions[instruction]["owner_instance_name"] = remote_info.bound_obj.instance_name - self.actions[instruction]["type"] = 'classmethod' if isinstance(remote_info.obj, classmethod) else '' - self.actions[instruction]["signature"] = get_signature(remote_info.obj)[0] - except KeyError: - pass - elif remote_info.isproperty: - path_without_RW = instruction.rsplit('/', 1)[0] - if path_without_RW not in self.properties: - self.properties[path_without_RW] = instance.__class__.properties.webgui_info(remote_info.obj)[remote_info.obj.name] - self.properties[path_without_RW]["remote_info"] = self.properties[path_without_RW]["remote_info"].json() - self.properties[path_without_RW]["instruction"] = path_without_RW - self.properties[path_without_RW]["remote_info"]["http_method"] = instance.httpserver_resources[path_without_RW].instructions.supported_methods() - """ - The instruction part has to be cleaned up to be called as fullpath. Setting the full path back into - remote_info is not correct because the unbound method is used by multiple instances. - """ - self.properties[path_without_RW]["owner_instance_name"] = remote_info.bound_obj.instance_name - return self + from .thing import Thing + + assert isinstance(instance, Thing), f"got invalid type {type(instance)}" + our_TD = instance.get_thing_description() + our_TD["inheritance"] = [class_.__name__ for class_ in instance.__class__.mro()] + + for instruction, remote_info in instance.instance_resources.items(): + if remote_info.isaction and remote_info.obj_name in our_TD["actions"]: + if isinstance(remote_info.obj, classmethod): + our_TD["actions"][remote_info.obj_name]["type"] = 'classmethod' + our_TD["actions"][remote_info.obj_name]["signature"] = get_signature(remote_info.obj)[0] + elif remote_info.isproperty and remote_info.obj_name in our_TD["properties"]: + our_TD["properties"][remote_info.obj_name].update(instance.__class__.properties.webgui_info(remote_info.obj)[remote_info.obj_name]) + return our_TD + def get_organised_resources(instance): diff --git a/hololinked/server/eventloop.py b/hololinked/server/eventloop.py index 4e64f98..e605ff4 100644 --- a/hololinked/server/eventloop.py +++ b/hololinked/server/eventloop.py @@ -188,7 +188,7 @@ def instantiate(self, id : str, kwargs : typing.Dict = {}): if not self.threaded: self.thing_executor_loop.call_soon(asyncio.create_task(lambda : self.run_single_target(instance))) else: - _thing_executor = threading.Thread(target=self.run_thing_executor, args=([instance],)) + _thing_executor = threading.Thread(target=self.run_things_executor, args=([instance],)) _thing_executor.start() def run(self): @@ -248,6 +248,7 @@ def run_things_executor(self, things): Please dont call this method when the async loop is already running. """ thing_executor_loop = self.get_async_loop() + self.thing_executor_loop = thing_executor_loop # atomic assignment for thread safety self.logger.info(f"starting thing executor loop in thread {threading.get_ident()} for {[obj.instance_name for obj in things]}") thing_executor_loop.run_until_complete( asyncio.gather(*[self.run_single_target(instance) for instance in things]) diff --git a/hololinked/server/logger.py b/hololinked/server/logger.py index 9222032..1db610d 100644 --- a/hololinked/server/logger.py +++ b/hololinked/server/logger.py @@ -8,7 +8,7 @@ from .constants import HTTP_METHODS from .events import Event -from .property import Property +from .properties import List from .properties import Integer, Number from .thing import Thing as RemoteObject from .action import action as remote_method @@ -193,22 +193,22 @@ async def _async_push_diff_logs(self) -> None: self.diff_logs.clear() self._owner.logger.info(f"ending log events.") - debug_logs = Property(readonly=True, URL_path='/logs/debug', fget=lambda self: self._debug_logs, + debug_logs = List(default=[], readonly=True, URL_path='/logs/debug', fget=lambda self: self._debug_logs, doc="logs at logging.DEBUG level") - warn_logs = Property(readonly=True, URL_path='/logs/warn', fget=lambda self: self._warn_logs, + warn_logs = List(default=[], readonly=True, URL_path='/logs/warn', fget=lambda self: self._warn_logs, doc="logs at logging.WARN level") - info_logs = Property(readonly=True, URL_path='/logs/info', fget=lambda self: self._info_logs, + info_logs = List(default=[], readonly=True, URL_path='/logs/info', fget=lambda self: self._info_logs, doc="logs at logging.INFO level") - error_logs = Property(readonly=True, URL_path='/logs/error', fget=lambda self: self._error_logs, + error_logs = List(default=[], readonly=True, URL_path='/logs/error', fget=lambda self: self._error_logs, doc="logs at logging.ERROR level") - critical_logs = Property(readonly=True, URL_path='/logs/critical', fget=lambda self: self._critical_logs, + critical_logs = List(default=[], readonly=True, URL_path='/logs/critical', fget=lambda self: self._critical_logs, doc="logs at logging.CRITICAL level") - execution_logs = Property(readonly=True, URL_path='/logs/execution', fget=lambda self: self._execution_logs, + execution_logs = List(default=[], readonly=True, URL_path='/logs/execution', fget=lambda self: self._execution_logs, doc="logs at all levels accumulated in order of collection/execution") diff --git a/hololinked/server/property.py b/hololinked/server/property.py index 024314b..6bdc031 100644 --- a/hololinked/server/property.py +++ b/hololinked/server/property.py @@ -256,8 +256,7 @@ def comparator(self, func : typing.Callable) -> typing.Callable: __property_info__ = [ 'allow_None' , 'class_member', 'db_init', 'db_persist', 'db_commit', 'deepcopy_default', 'per_instance_descriptor', - 'default', 'doc', 'constant', - 'metadata', 'name', 'readonly' + 'state', 'precedence', 'constant', 'default' # 'scada_info', 'property_type' # descriptor related info is also necessary ] @@ -319,9 +318,7 @@ def webgui_info(self, for_remote_params : typing.Union[Property, typing.Dict[str for param in objects.values(): state = param.__getstate__() info[param.name] = dict( - remote_info = state.get("_remote_info", None).to_dataclass(), - type = param.__class__.__name__, - owner = param.owner.__name__ + python_type = param.__class__.__name__, ) for field in __property_info__: info[param.name][field] = state.get(field, None) diff --git a/hololinked/server/td.py b/hololinked/server/td.py index e553870..2293169 100644 --- a/hololinked/server/td.py +++ b/hololinked/server/td.py @@ -10,7 +10,7 @@ from .properties import * from .property import Property from .thing import Thing -from .eventloop import EventLoop + @@ -263,7 +263,7 @@ def generate_schema(self, property : Property, owner : Thing, authority : str) - elif self._custom_schema_generators.get(property, NotImplemented) is not NotImplemented: schema = self._custom_schema_generators[property]() else: - raise TypeError(f"WoT schema generator for this descriptor/property is not implemented. type {type(property)}") + raise TypeError(f"WoT schema generator for this descriptor/property is not implemented. name {property.name} & type {type(property)}") schema.build(property=property, owner=owner, authority=authority) return schema.asdict() @@ -716,11 +716,10 @@ class ThingDescription(Schema): schemaDefinitions : typing.Optional[typing.List[DataSchema]] skip_properties = ['expose', 'httpserver_resources', 'zmq_resources', 'gui_resources', - 'events', 'debug_logs', 'warn_logs', 'info_logs', 'error_logs', 'critical_logs', - 'thing_description', 'maxlen', 'execution_logs', 'GUI', 'object_info' ] + 'events', 'thing_description', 'GUI', 'object_info' ] - skip_actions = ['_set_properties', '_get_properties', '_add_property', 'push_events', 'stop_events', - 'get_postman_collection', 'get_thing_description'] + skip_actions = ['_set_properties', '_get_properties', '_add_property', '_get_properties_in_db', + 'push_events', 'stop_events', 'get_postman_collection', 'get_thing_description'] # not the best code and logic, but works for now diff --git a/hololinked/server/thing.py b/hololinked/server/thing.py index bbdcf0f..10cce80 100644 --- a/hololinked/server/thing.py +++ b/hololinked/server/thing.py @@ -8,17 +8,17 @@ import zmq.asyncio from ..param.parameterized import Parameterized, ParameterizedMetaclass, edit_constant as edit_constant_parameters -from .constants import (JSON, LOGLEVEL, ZMQ_PROTOCOLS, HTTP_METHODS) +from .constants import (JSON, LOGLEVEL, ZMQ_PROTOCOLS, HTTP_METHODS, JSONSerializable) from .database import ThingDB, ThingInformation from .serializers import _get_serializer_from_user_given_options, BaseSerializer, JSONSerializer from .schema_validators import BaseSchemaValidator, JsonSchemaValidator from .exceptions import BreakInnerLoop from .action import action -from .dataklasses import GUIResources, HTTPResource, ZMQResource, get_organised_resources +from .dataklasses import HTTPResource, ZMQResource, build_our_temp_TD, get_organised_resources from .utils import get_default_logger, getattr_without_descriptor_read from .property import Property, ClassProperties from .properties import String, ClassSelector, Selector, TypedKeyMappingsConstrainedDict -from .zmq_message_brokers import RPCServer, ServerTypes, AsyncPollingZMQServer, EventPublisher +from .zmq_message_brokers import RPCServer, ServerTypes, EventPublisher from .state_machine import StateMachine from .events import Event @@ -111,7 +111,6 @@ class Thing(Parameterized, metaclass=ThingMeta): state = String(default=None, allow_None=True, URL_path='/state', readonly=True, observable=True, fget=lambda self : self.state_machine.current_state if hasattr(self, 'state_machine') else None, doc="current state machine's state if state machine present, None indicates absence of state machine.") #type: typing.Optional[str] - httpserver_resources = Property(readonly=True, URL_path='/resources/http-server', doc="object's resources exposed to HTTP client (through ``hololinked.server.HTTPServer.HTTPServer``)", fget=lambda self: self._httpserver_resources ) # type: typing.Dict[str, HTTPResource] @@ -121,7 +120,7 @@ class Thing(Parameterized, metaclass=ThingMeta): gui_resources = Property(readonly=True, URL_path='/resources/portal-app', doc="""object's data read by hololinked-portal GUI client, similar to http_resources but differs in details.""", - fget=lambda self: GUIResources().build(self)) # type: typing.Dict[str, typing.Any] + fget=lambda self: build_our_temp_TD(self)) # type: typing.Dict[str, typing.Any] GUI = Property(default=None, allow_None=True, URL_path='/resources/web-gui', fget = lambda self : self._gui, doc="GUI specified here will become visible at GUI tab of hololinked-portal dashboard tool") object_info = Property(doc="contains information about this object like the class name, script location etc.", @@ -371,6 +370,28 @@ def _set_properties(self, **values : typing.Dict[str, typing.Any]) -> None: ex.__notes__ = errors raise ex from None + @action(URL_path='/properties/db', http_method=HTTP_METHODS.GET) + def _get_properties_in_db(self) -> typing.Dict[str, JSONSerializable]: + """ + get all properties in the database + + Returns + ------- + Dict[str, JSONSerializable] + dictionary of property names and their values + """ + if not hasattr(self, 'db_engine'): + return {} + props = self.db_engine.get_all_properties() + final_list = {} + for name, prop in props.items(): + try: + self.http_serializer.dumps(prop) + final_list[name] = prop + except Exception as ex: + self.logger.error(f"could not serialize property {name} to JSON due to error {str(ex)}, skipping this property") + return final_list + @action(URL_path='/properties', http_method=HTTP_METHODS.POST) def _add_property(self, name : str, prop : JSON) -> None: """ From 1f7c58ed9add4f981e89d8c79de27f4f7d565e19 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:41:46 +0200 Subject: [PATCH 24/36] added personal discord --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f1063d..1708c18 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ For those familiar with RPC & web development - This package is an implementatio [![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked)
-[![email](https://img.shields.io/badge/email%20me-brown)](mailto:vignesh.vaidyanathan@hololinked.dev) - +[![email](https://img.shields.io/badge/email%20me-brown)](mailto:vignesh.vaidyanathan@hololinked.dev) [![find me on discord](https://img.shields.io/badge/find_me_on_discord-brown)](https://discord.com/users/1178428338746966066) ### To Install From pip - ``pip install hololinked`` From 041cd38ee8ba812767d833b9d0c85d1f8fc46ab2 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:41:43 +0200 Subject: [PATCH 25/36] update examples HEAD --- examples | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples b/examples index 52897aa..2c33b59 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 52897aa0bd3e84af3a3fb41eda5c3a2e14cf4024 +Subproject commit 2c33b591f055c09eda27186081c2b4597d6dcfe2 From c30583fe96978596c66e11242368d69c5d33a5aa Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:41:18 +0200 Subject: [PATCH 26/36] fixes state machine non-inclusion in TD when no state machine present --- README.md | 10 +++++----- hololinked/server/td.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1708c18..4ad485e 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ ### Description -For beginners - `hololinked` is a server side pythonic package suited for instrumentation control and data acquisition over network, especially with HTTP. If you have a requirement to control and capture data from your hardware/instrumentation, show the data in a browser/dashboard, provide a GUI or run automated scripts, `hololinked` can help. Even for isolated applications or a small lab setup without networking concepts, one can still separate the concerns of the tools that interact with the hardware & the hardware itself. +For beginners - `hololinked` is a server side pythonic tool suited for instrumentation control and data acquisition over network, especially with HTTP. If you have a requirement to control and capture data from your hardware/instrumentation, show the data in a browser/dashboard, provide a GUI or run automated scripts, `hololinked` can help. Even for isolated applications or a small lab setup without networking concepts, one can still separate the concerns of the tools that interact with the hardware & the hardware itself.

For those familiar with RPC & web development - This package is an implementation of a ZeroMQ-based Object Oriented RPC with customizable HTTP end-points. A dual transport in both ZMQ and HTTP is provided to maximize flexibility in data type, serialization and speed, although HTTP is preferred for networked applications. If one is looking for an object oriented approach towards creating components within a control or data acquisition system, or an IoT device, one may consider this package. [![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked)
[![email](https://img.shields.io/badge/email%20me-brown)](mailto:vignesh.vaidyanathan@hololinked.dev) [![find me on discord](https://img.shields.io/badge/find_me_on_discord-brown)](https://discord.com/users/1178428338746966066) + ### To Install From pip - ``pip install hololinked`` @@ -20,7 +21,7 @@ Or, clone the repository (develop branch for latest codebase) and install `pip i `hololinked` is compatible with the [Web of Things](https://www.w3.org/WoT/) recommended pattern for developing hardware/instrumentation control software. Each device or thing can be controlled systematically when their design in software is segregated into properties, actions and events. In object oriented terms: - the hardware is (generally) represented by a class -- properties are validated get-set attributes of the class which may be used to model hardware settings, hold captured/computed data or generic network accessible quantities +- properties are validated get-set attributes of the class which may be used to model settings, hold captured/computed data or generic network accessible quantities - actions are methods which issue commands like connect/disconnect, execute a control routine, start/stop measurement, or run arbitray python logic - events can asynchronously communicate/push (arbitrary) data to a client (say, a GUI), like alarm messages, streaming measured quantities etc. @@ -130,8 +131,7 @@ Those familiar with Web of Things (WoT) terminology may note that these properti ``` If you are not familiar with Web of Things or the term "property affordance", consider the above JSON as a description of what the property represents and how to interact with it from somewhere else. Such a JSON is both human-readable, yet consumable -by a client provider to create a client object to interact with the property in the way the property demands. You, as the developer, -only need to use the client. +by a client provider to create a client object to interact with the property. The URL path segment `../spectrometer/..` in href field is taken from the `instance_name` which was specified in the `__init__`. This is a mandatory key word argument to the parent class `Thing` to generate a unique name/id for the instance. One should use URI compatible strings. @@ -271,7 +271,7 @@ what the event represents and how to subscribe to it) with subprotocol SSE (HTTP ``` > data schema ("data" field above which describes the event payload) are optional and discussed later -Events follow a pub-sub model with '1 publisher to N subscribers' technique per `Event` object, both through ZMQ and HTTP SSE. +Events follow a pub-sub model with '1 publisher to N subscribers' per `Event` object, both through ZMQ and HTTP SSE. Although the code is the very familiar & age-old RPC server style, one can directly specify HTTP methods and URL path for each property, action and event. A configurable HTTP Server is already available (from `hololinked.server.HTTPServer`) which redirects HTTP requests to the object according to the specified HTTP API on the properties, actions and events. To plug in a HTTP server: diff --git a/hololinked/server/td.py b/hololinked/server/td.py index 2293169..1a549fc 100644 --- a/hololinked/server/td.py +++ b/hololinked/server/td.py @@ -1,5 +1,4 @@ -import inspect -import typing +import typing, inspect from dataclasses import dataclass, field @@ -10,6 +9,7 @@ from .properties import * from .property import Property from .thing import Thing +from .state_machine import StateMachine @@ -757,8 +757,8 @@ def add_interaction_affordances(self): if (resource.isproperty and resource.obj_name not in self.properties and resource.obj_name not in self.skip_properties and hasattr(resource.obj, "_remote_info") and resource.obj._remote_info is not None): - if (resource.obj_name == 'state' and hasattr(self.instance, 'state_machine') is None and - self.instance.state_machine is not None): + if (resource.obj_name == 'state' and (not hasattr(self.instance, 'state_machine') or + not isinstance(self.instance.state_machine, StateMachine))): continue self.properties[resource.obj_name] = PropertyAffordance.generate_schema(resource.obj, self.instance, self.authority) From 39a60329f82f261c6779fbd1170c9008bb7a16c9 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:45:08 +0200 Subject: [PATCH 27/36] added raspberry pi example --- examples | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples b/examples index 2c33b59..8a70beb 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 2c33b591f055c09eda27186081c2b4597d6dcfe2 +Subproject commit 8a70bebddd4da7d66674ec3e3da469db120dab70 From 7be429a2696aa6264bfb2c7662b35577e21b7072 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:56:02 +0200 Subject: [PATCH 28/36] update README --- README.md | 11 +++++------ hololinked/server/properties.py | 10 +++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4ad485e..1c13871 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ ### Description -For beginners - `hololinked` is a server side pythonic tool suited for instrumentation control and data acquisition over network, especially with HTTP. If you have a requirement to control and capture data from your hardware/instrumentation, show the data in a browser/dashboard, provide a GUI or run automated scripts, `hololinked` can help. Even for isolated applications or a small lab setup without networking concepts, one can still separate the concerns of the tools that interact with the hardware & the hardware itself. -

-For those familiar with RPC & web development - This package is an implementation of a ZeroMQ-based Object Oriented RPC with customizable HTTP end-points. A dual transport in both ZMQ and HTTP is provided to maximize flexibility in data type, serialization and speed, although HTTP is preferred for networked applications. If one is looking for an object oriented approach towards creating components within a control or data acquisition system, or an IoT device, one may consider this package. +`hololinked` is a server side pythonic tool suited for instrumentation control and data acquisition over network, especially with HTTP. If you have a requirement to control and capture data from your hardware/instrumentation, show the data in a browser/dashboard, provide a GUI or run automated scripts, `hololinked` can help. Even for isolated applications or a small lab setup without networking concepts, one can still separate the concerns of the tools that interact with the hardware & the hardware itself. [![Documentation Status](https://readthedocs.org/projects/hololinked/badge/?version=latest)](https://hololinked.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/hololinked?label=pypi%20package)](https://pypi.org/project/hololinked/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked)](https://pypistats.org/packages/hololinked) [![codecov](https://codecov.io/gh/VigneshVSV/hololinked/graph/badge.svg?token=JF1928KTFE)](https://codecov.io/gh/VigneshVSV/hololinked)
@@ -329,11 +327,12 @@ Again, please check examples or the code for explanations. Documentation is bein - improving accuracy of Thing Descriptions - cookie credentials for authentication - as a workaround until credentials are supported, use `allowed_clients` argument on HTTP server which restricts access based on remote IP supplied with the HTTP headers. +### Internals + +This package is an implementation of a ZeroMQ-based Object Oriented RPC with customizable HTTP end-points. A dual transport in both ZMQ and HTTP is provided to maximize flexibility in data type, serialization and speed, although HTTP is preferred for networked applications. If one is looking for an object oriented approach towards creating components within a control or data acquisition system, or an IoT device, one may consider this package. + ### Some Day In Future - mongo DB support for DB operations - HTTP 2.0 -### Contact - -Contributors welcome for all my projects related to hololinked including web apps. Please write to my contact email available at my [website](https://hololinked.dev). diff --git a/hololinked/server/properties.py b/hololinked/server/properties.py index 8bf9a91..3fe33ba 100644 --- a/hololinked/server/properties.py +++ b/hololinked/server/properties.py @@ -23,7 +23,15 @@ class String(Property): - """A string property with optional regular expression (regex) matching.""" + """A string property with optional regular expression (regex) matching. + + Parameters + ---------- + default: str + default value of the string, if not None or empty + regex: str + the regular expression to match during validation + """ type = 'string' # TD type From 504cde2981cff006b60c7b7edf9b5f8871877eca Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Thu, 8 Aug 2024 21:48:31 +0200 Subject: [PATCH 29/36] updated client API to resemble wot operations --- hololinked/client/proxy.py | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/hololinked/client/proxy.py b/hololinked/client/proxy.py index 7f09f22..3637da7 100644 --- a/hololinked/client/proxy.py +++ b/hololinked/client/proxy.py @@ -33,7 +33,7 @@ class ObjectProxy: when True, remote object is located and its resources are loaded. Otherwise, only the client is initialised. protocol: str ZMQ protocol used to connect to server. Unlike the server, only one can be specified. - **kwargs: + **kwargs: async_mixin: bool, default False whether to use both synchronous and asynchronous clients. serializer: BaseSerializer @@ -71,12 +71,12 @@ def __init__(self, instance_name : str, protocol : str = ZMQ_PROTOCOLS.IPC, invo # compose ZMQ client in Proxy client so that all sending and receiving is # done by the ZMQ client and not by the Proxy client directly. Proxy client only # bothers mainly about __setattr__ and _getattr__ - self._async_zmq_client = None - self._zmq_client = SyncZMQClient(instance_name, self.identity, client_type=PROXY, protocol=protocol, + self.async_zmq_client = None + self.zmq_client = SyncZMQClient(instance_name, self.identity, client_type=PROXY, protocol=protocol, zmq_serializer=kwargs.get('serializer', None), handshake=load_thing, logger=self.logger, **kwargs) if kwargs.get("async_mixin", False): - self._async_zmq_client = AsyncZMQClient(instance_name, self.identity + '|async', client_type=PROXY, protocol=protocol, + self.async_zmq_client = AsyncZMQClient(instance_name, self.identity + '|async', client_type=PROXY, protocol=protocol, zmq_serializer=kwargs.get('serializer', None), handshake=load_thing, logger=self.logger, **kwargs) if load_thing: @@ -108,11 +108,11 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - del self + pass def __bool__(self) -> bool: try: - self._zmq_client.handshake(num_of_tries=10) + self.zmq_client.handshake(num_of_tries=10) return True except RuntimeError: return False @@ -121,12 +121,12 @@ def __eq__(self, other) -> bool: if other is self: return True return (isinstance(other, ObjectProxy) and other.instance_name == self.instance_name and - other._zmq_client.protocol == self._zmq_client.protocol) + other.zmq_client.protocol == self.zmq_client.protocol) def __ne__(self, other) -> bool: if other and isinstance(other, ObjectProxy): return (other.instance_name != self.instance_name or - other._zmq_client.protocol != self._zmq_client.protocol) + other.zmq_client.protocol != self.zmq_client.protocol) return True def __hash__(self) -> int: @@ -164,7 +164,7 @@ def set_execution_timeout(self, value : typing.Union[float, int]) -> None: ) - def invoke(self, method : str, oneway : bool = False, noblock : bool = False, + def invoke_action(self, method : str, oneway : bool = False, noblock : bool = False, *args, **kwargs) -> typing.Any: """ call a method specified by name on the server with positional/keyword arguments @@ -207,7 +207,7 @@ def invoke(self, method : str, oneway : bool = False, noblock : bool = False, return method(*args, **kwargs) - async def async_invoke(self, method : str, *args, **kwargs) -> typing.Any: + async def async_invoke_action(self, method : str, *args, **kwargs) -> typing.Any: """ async(io) call a method specified by name on the server with positional/keyword arguments. noblock and oneway not supported for async calls. @@ -241,7 +241,7 @@ async def async_invoke(self, method : str, *args, **kwargs) -> typing.Any: return await method.async_call(*args, **kwargs) - def get_property(self, name : str, noblock : bool = False) -> typing.Any: + def read_property(self, name : str, noblock : bool = False) -> typing.Any: """ get property specified by name on server. @@ -270,7 +270,7 @@ def get_property(self, name : str, noblock : bool = False) -> typing.Any: return prop.get() - def set_property(self, name : str, value : typing.Any, oneway : bool = False, + def write_property(self, name : str, value : typing.Any, oneway : bool = False, noblock : bool = False) -> None: """ set property specified by name on server with specified value. @@ -307,7 +307,7 @@ def set_property(self, name : str, value : typing.Any, oneway : bool = False, prop.set(value) - async def async_get_property(self, name : str) -> None: + async def async_read_property(self, name : str) -> None: """ async(io) get property specified by name on server. @@ -329,7 +329,7 @@ async def async_get_property(self, name : str) -> None: return await prop.async_get() - async def async_set_property(self, name : str, value : typing.Any) -> None: + async def async_write_property(self, name : str, value : typing.Any) -> None: """ async(io) set property specified by name on server with specified value. noblock and oneway not supported for async calls. @@ -354,7 +354,7 @@ async def async_set_property(self, name : str, value : typing.Any) -> None: await prop.async_set(value) - def get_properties(self, names : typing.List[str], noblock : bool = False) -> typing.Any: + def read_multiple_properties(self, names : typing.List[str], noblock : bool = False) -> typing.Any: """ get properties specified by list of names. @@ -381,7 +381,7 @@ def get_properties(self, names : typing.List[str], noblock : bool = False) -> ty return method(names=names) - def set_properties(self, oneway : bool = False, noblock : bool = False, + def write_multiple_properties(self, oneway : bool = False, noblock : bool = False, **properties : typing.Dict[str, typing.Any]) -> None: """ set properties whose name is specified by keys of a dictionary @@ -418,7 +418,7 @@ def set_properties(self, oneway : bool = False, noblock : bool = False, return method(**properties) - async def async_get_properties(self, names) -> None: + async def async_read_multiple_properties(self, names) -> None: """ async(io) get properties specified by list of names. no block gets are not supported for asyncio. @@ -438,7 +438,7 @@ async def async_get_properties(self, names) -> None: return await method.async_call(names=names) - async def async_set_properties(self, **properties) -> None: + async def async_write_multiple_properties(self, **properties) -> None: """ async(io) set properties whose name is specified by keys of a dictionary @@ -523,9 +523,9 @@ def read_reply(self, message_id : bytes, timeout : typing.Optional[float] = 5000 obj = self._noblock_messages.get(message_id, None) if not obj: raise ValueError('given message id not a one way call or invalid.') - reply = self._zmq_client._reply_cache.get(message_id, None) + reply = self.zmq_client._reply_cache.get(message_id, None) if not reply: - reply = self._zmq_client.recv_reply(message_id=message_id, timeout=timeout, + reply = self.zmq_client.recv_reply(message_id=message_id, timeout=timeout, raise_client_side_exception=True) if not reply: raise ReplyNotArrivedError(f"could not fetch reply within timeout for message id '{message_id}'") @@ -541,7 +541,7 @@ def load_thing(self): """ Get exposed resources from server (methods, properties, events) and remember them as attributes of the proxy. """ - fetch = _RemoteMethod(self._zmq_client, CommonRPC.zmq_resource_read(instance_name=self.instance_name), + fetch = _RemoteMethod(self.zmq_client, CommonRPC.zmq_resource_read(instance_name=self.instance_name), invokation_timeout=self._invokation_timeout) # type: _RemoteMethod reply = fetch() # type: typing.Dict[str, typing.Dict[str, typing.Any]] @@ -559,15 +559,15 @@ def load_thing(self): elif not isinstance(data, (ZMQResource, ServerSentEvent)): raise RuntimeError("Logic error - deserialized info about server not instance of hololinked.server.data_classes.ZMQResource") if data.what == ResourceTypes.ACTION: - _add_method(self, _RemoteMethod(self._zmq_client, data.instruction, self.invokation_timeout, - self.execution_timeout, data.argument_schema, self._async_zmq_client, self._schema_validator), data) + _add_method(self, _RemoteMethod(self.zmq_client, data.instruction, self.invokation_timeout, + self.execution_timeout, data.argument_schema, self.async_zmq_client, self._schema_validator), data) elif data.what == ResourceTypes.PROPERTY: - _add_property(self, _Property(self._zmq_client, data.instruction, self.invokation_timeout, - self.execution_timeout, self._async_zmq_client), data) + _add_property(self, _Property(self.zmq_client, data.instruction, self.invokation_timeout, + self.execution_timeout, self.async_zmq_client), data) elif data.what == ResourceTypes.EVENT: assert isinstance(data, ServerSentEvent) - event = _Event(self._zmq_client, data.name, data.obj_name, data.unique_identifier, data.socket_address, - serializer=self._zmq_client.zmq_serializer, logger=self.logger) + event = _Event(self.zmq_client, data.name, data.obj_name, data.unique_identifier, data.socket_address, + serializer=self.zmq_client.zmq_serializer, logger=self.logger) _add_event(self, event, data) self.__dict__[data.name] = event From b5581d485df9a5406b1418bff03203b703dfec61 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:33:25 +0200 Subject: [PATCH 30/36] bug fix object proxy client --- hololinked/client/proxy.py | 2 +- tests/test_rpc.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hololinked/client/proxy.py b/hololinked/client/proxy.py index 3637da7..50f8deb 100644 --- a/hololinked/client/proxy.py +++ b/hololinked/client/proxy.py @@ -52,7 +52,7 @@ class ObjectProxy: _own_attrs = frozenset([ '__annotations__', - '_zmq_client', '_async_zmq_client', '_allow_foreign_attributes', + 'zmq_client', 'async_zmq_client', '_allow_foreign_attributes', 'identity', 'instance_name', 'logger', 'execution_timeout', 'invokation_timeout', '_execution_timeout', '_invokation_timeout', '_events', '_noblock_messages', '_schema_validator' diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 5b16853..5bec84e 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -191,7 +191,7 @@ async def message_coro(): nonlocal success, client for i in range(2000): value = gen_random_data() - ret = await client.async_invoke('test_echo', value) + ret = await client.async_invoke_action('test_echo', value) # print("async", 1, i, value, ret) if value != ret: print("error", "async", 1, i, value, ret) @@ -211,7 +211,7 @@ async def message_coro(id): nonlocal success, client for i in range(1000): value = gen_random_data() - ret = await client.async_invoke('test_echo', value) + ret = await client.async_invoke_action('test_echo', value) # print("multi-coro", id, i, value, ret) if value != ret: print("error", "multi-coro", id, i, value, ret) From 3d3c8c2e2e84f7e7ca4dcfe97a4d3c9023ebfd6e Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:36:26 +0200 Subject: [PATCH 31/36] bug fix object proxy client --- hololinked/__init__.py | 2 +- setup.py | 2 +- tests/test_property.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hololinked/__init__.py b/hololinked/__init__.py index 3ced358..b5fdc75 100644 --- a/hololinked/__init__.py +++ b/hololinked/__init__.py @@ -1 +1 @@ -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/setup.py b/setup.py index 59172c8..e4083a5 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setuptools.setup( name="hololinked", - version="0.2.1", + version="0.2.2", author="Vigneh Vaidyanathan", author_email="vignesh.vaidyanathan@hololinked.dev", description="A ZMQ-based Object Oriented RPC tool-kit with HTTP support for instrument control/data acquisition or controlling generic python objects.", diff --git a/tests/test_property.py b/tests/test_property.py index 8583a3d..4115fe6 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -97,7 +97,7 @@ def test_1_client_api(self): def test_2_RW_multiple_properties(self): # Test partial list of read write properties - self.thing_client.set_properties( + self.thing_client.write_multiple_properties( number_prop=15, string_prop='foobar' ) @@ -108,7 +108,7 @@ def test_2_RW_multiple_properties(self): self.thing_client.selector_prop = 'b' self.thing_client.number_prop = -15 - props = self.thing_client.get_properties(names=['selector_prop', 'int_prop', + props = self.thing_client.read_multiple_properties(names=['selector_prop', 'int_prop', 'number_prop', 'string_prop']) self.assertEqual(props['selector_prop'], 'b') self.assertEqual(props['int_prop'], 5) @@ -210,7 +210,7 @@ def test_4_db_operations(self): self.assertEqual(thing.db_commit_number_prop, TestThing.db_commit_number_prop.default) # check db init prop with a different value in database apart from default - thing.db_engine.set_property('db_init_int_prop', 101) + thing.db_engine.write_property('db_init_int_prop', 101) del thing thing = TestThing(instance_name='test-db-operations', use_default_db=True, log_level=logging.WARN) self.assertEqual(thing.db_init_int_prop, 101) From f20ffc89dbb7e330c2c7c9b6a971b7f49a771063 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:42:49 +0200 Subject: [PATCH 32/36] update changelog, doc and examples --- CHANGELOG.md | 7 +++++++ doc | 2 +- tests/test_property.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2afd17..58d8e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - cookie auth & its specification in TD +## [v0.2.2] - 2024-08-09 + +- thing control panel works better with the server side and support observable properties +- `ObjectProxy` client API has been improved to resemble WoT operations better, for examplem `get_property` is now +called `read_property`, `set_properties` is now called `write_multiple_properties`. +- `ObjectProxy` client reliability for poorly written server side actions improved + ## [v0.2.1] - 2024-07-21 ### Added diff --git a/doc b/doc index 68e1be2..a064864 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit 68e1be22ce184ec0c28eafb9b8a715a1a6bc9d33 +Subproject commit a064864119dd4270a69b38621d79678a9f1b8069 diff --git a/tests/test_property.py b/tests/test_property.py index 4115fe6..bfa1710 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -210,7 +210,7 @@ def test_4_db_operations(self): self.assertEqual(thing.db_commit_number_prop, TestThing.db_commit_number_prop.default) # check db init prop with a different value in database apart from default - thing.db_engine.write_property('db_init_int_prop', 101) + thing.db_engine.set_property('db_init_int_prop', 101) del thing thing = TestThing(instance_name='test-db-operations', use_default_db=True, log_level=logging.WARN) self.assertEqual(thing.db_init_int_prop, 101) From 37490d95c4cf4267dfe9038b4694e2c5232ad775 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:00:45 +0200 Subject: [PATCH 33/36] force install only latest version of package from testpypi --- .github/workflows/test-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index 237c69c..e7b79b9 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -22,11 +22,11 @@ jobs: - name: Install dependencies run: | pip install jsonschema - pip install -i https://test.pypi.org/simple/ hololinked + pip install -i https://test.pypi.org/simple/ --upgrade hololinked - name: Run unit tests to verify if the release to TestPyPI is working run: | python -m unittest discover -s tests -p 'test_*.py' - \ No newline at end of file + From 207c81683cf3ab4db0aab216ba760398c8009412 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:06:01 +0200 Subject: [PATCH 34/36] Update test-release.yml --- .github/workflows/test-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index e7b79b9..ffebff5 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | pip install jsonschema - pip install -i https://test.pypi.org/simple/ --upgrade hololinked + pip install -i --upgrade --upgrade-strategy eager https://test.pypi.org/simple/ hololinked - name: Run unit tests to verify if the release to TestPyPI is working run: | From 4717d4efcf3641670f70040f1ed04c4a1be7862d Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:08:53 +0200 Subject: [PATCH 35/36] Update test-release.yml --- .github/workflows/test-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index ffebff5..66e3ba5 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | pip install jsonschema - pip install -i --upgrade --upgrade-strategy eager https://test.pypi.org/simple/ hololinked + pip install -i https://test.pypi.org/simple/ hololinked==0.2.2 - name: Run unit tests to verify if the release to TestPyPI is working run: | From 0577da1a47473a44e7de1393f1c6aab2c8febc9c Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:35:53 +0200 Subject: [PATCH 36/36] update test release to install requirements before --- .github/workflows/test-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index 66e3ba5..b428a53 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -21,8 +21,9 @@ jobs: - name: Install dependencies run: | + pip install -r tests/requirements.txt pip install jsonschema - pip install -i https://test.pypi.org/simple/ hololinked==0.2.2 + pip install -i https://test.pypi.org/simple/ hololinked - name: Run unit tests to verify if the release to TestPyPI is working run: |