From c135687b40b05ea24b0b44e3dbbc507f1627e625 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:56:28 +0200 Subject: [PATCH 01/14] update doc to latest commit & changelog to latest status --- .gitlab-ci.yml | 9 --------- CHANGELOG.md | 4 ++++ doc | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 4ebf85e9..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,9 +0,0 @@ -rules: - - if: '$CI_COMMIT_BRANCH == "main-next-release"' - -codestyle: - image: python:3.11 - stage: test - script: - - pip install uv - - uvx ruff check hololinked\core\thing.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bd209417..d5cdfa21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ✓ means ready to try +## [v0.3.3] - 2025-09-25 + +- updates API reference largely to latest version + ## [v0.3.2] - 2025-09-21 - adds TD security definition for BCryptBasicSecurity and ArgsBasicSecurity diff --git a/doc b/doc index 633d018d..afa0e647 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit 633d018dabfa5c0557100bee9fcb1c1c3e364bdf +Subproject commit afa0e64706dedec5a15c9c46f465f2ac0f3258ec From eb0d8c94bcf8230e315e46a0b0e323c13868f81c Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:46:06 +0200 Subject: [PATCH 02/14] add logic to overwrite form and carry over content type from the ZMQ layer --- hololinked/server/http/__init__.py | 30 +++++++--- hololinked/server/http/handlers.py | 79 +++++++++++++++++-------- hololinked/td/interaction_affordance.py | 24 ++++++++ tests/helper-scripts/design_script.py | 11 +++- 4 files changed, 107 insertions(+), 37 deletions(-) diff --git a/hololinked/server/http/__init__.py b/hololinked/server/http/__init__.py index 1e397e95..1c236f64 100644 --- a/hololinked/server/http/__init__.py +++ b/hololinked/server/http/__init__.py @@ -4,6 +4,7 @@ import socket import ssl import typing +from copy import deepcopy from tornado import ioloop from tornado.web import Application from tornado.httpserver import HTTPServer as TornadoHTTP1Server @@ -73,6 +74,7 @@ class HTTPServer(Parameterized): ) # type: int address = IPAddress(default="0.0.0.0", doc="IP address") # type: str + # protocol_version = Selector(objects=[1, 1.1, 2], default=2, # doc="for HTTP 2, SSL is mandatory. HTTP2 is recommended. \ # When no SSL configurations are provided, defaults to 1.1" ) # type: float @@ -103,8 +105,8 @@ class HTTPServer(Parameterized): allowed_clients = TypedList( item_type=str, doc="""Serves request and sets CORS only from these clients, other clients are rejected with 403. - Unlike pure CORS, the server resource is not even executed if the client is not - an allowed client. if None any client is served.""", + Unlike pure CORS, the server resource is not even executed if the client is not + an allowed client. if None any client is served.""", ) host = String( @@ -112,6 +114,7 @@ class HTTPServer(Parameterized): allow_None=True, doc="Host Server to subscribe to coordinate starting sequence of remote objects & web GUI", ) # type: str + # network_interface = String(default='Ethernet', # doc="Currently there is no logic to detect the IP addresss (as externally visible) correctly, \ # therefore please send the network interface name to retrieve the IP. If a DNS server is present, \ @@ -121,21 +124,21 @@ class HTTPServer(Parameterized): default=PropertyHandler, class_=(PropertyHandler, RPCHandler), isinstance=False, - doc="custom web request handler of your choice for property read-write & action execution", + doc="custom web request handler for property read-write", ) # type: typing.Union[RPCHandler, PropertyHandler] action_handler = ClassSelector( default=ActionHandler, class_=(ActionHandler, RPCHandler), isinstance=False, - doc="custom web request handler of your choice for property read-write & action execution", + doc="custom web request handler for actions", ) # type: typing.Union[RPCHandler, ActionHandler] event_handler = ClassSelector( default=EventHandler, class_=(EventHandler, RPCHandler), isinstance=False, - doc="custom event handler of your choice for handling events", + doc="custom event handler for sending HTTP SSE", ) # type: typing.Union[RPCHandler, EventHandler] schema_validator = ClassSelector( @@ -157,7 +160,7 @@ class HTTPServer(Parameterized): default=None, allow_None=True, doc="""Set CORS headers for the HTTP server. If set to False, CORS headers are not set. - This is useful when the server is used in a controlled environment where CORS is not needed.""", + This is useful when the server is used in a controlled environment where CORS is not needed.""", ) # type: bool def __init__( @@ -696,6 +699,7 @@ def add_interaction_affordances( for property in properties: if property in self: continue + self.server.logger.debug(f"adding property {property.name} for thing id {property.thing_id}") if property.thing_id is not None: path = f"/{property.thing_id}/{pep8_to_dashed_name(property.name)}" else: @@ -720,6 +724,7 @@ def add_interaction_affordances( for action in actions: if action in self: continue + self.server.logger.debug(f"adding action {action.name} for thing id {action.thing_id}") name = get_alternate_name(action.name) if action.thing_id is not None: path = f"/{action.thing_id}/{pep8_to_dashed_name(name)}" @@ -729,17 +734,26 @@ def add_interaction_affordances( for event in events: if event in self: continue + self.server.logger.debug(f"adding event {event.name} for thing id {event.thing_id}") if event.thing_id is not None: path = f"/{event.thing_id}/{pep8_to_dashed_name(event.name)}" else: path = f"/{pep8_to_dashed_name(event.name)}" self.server.add_event(URL_path=path, event=event, handler=self.server.event_handler) + + get_thing_model_action = next((action for action in actions if action.name == "get_thing_model"), None) + get_thing_description_action = deepcopy(get_thing_model_action) + get_thing_description_action.override_defaults(name="get_thing_description") self.server.add_action( URL_path=f"/{thing_id}/resources/wot-td" if thing_id else "/resources/wot-td", - action=next((action for action in actions if action.name == "get_thing_model"), None), + action=get_thing_description_action, http_method=("GET",), handler=ThingDescriptionHandler, ) + self.server.logger.debug( + f"added thing description action for thing id {thing_id if thing_id else 'unknown'} at path " + + f"{f'/{thing_id}/resources/wot-td' if thing_id else '/resources/wot-td'}" + ) def add_thing_instance(self, thing: Thing) -> None: """ @@ -904,7 +918,7 @@ def print_rules(self) -> None: def get_alternate_name(interaction_affordance_name: str) -> str: if interaction_affordance_name == "get_thing_model": - return "resources/thing-model" + return "resources/wot-tm" return interaction_affordance_name diff --git a/hololinked/server/http/handlers.py b/hololinked/server/http/handlers.py index ebfd925f..e8658403 100644 --- a/hololinked/server/http/handlers.py +++ b/hololinked/server/http/handlers.py @@ -26,6 +26,7 @@ ActionAffordance, EventAffordance, ) +from ...td.forms import Form try: from ..security import BcryptBasicSecurity @@ -691,7 +692,13 @@ async def get(self): operation=Operations.invokeaction, payload=SerializableData( value=dict( - ignore_errors=body.get("ignore_errors", False), skip_names=body.get("skip_names", []) + ignore_errors=body.get("ignore_errors", False), + skip_names=body.get("skip_names", []), + protocol=self.zmq_client_pool[ + self.zmq_client_pool.get_client_id_from_thing_id(self.resource.thing_id) + ] + .socket_address.split("://")[0] + .upper(), ), ), ) @@ -713,85 +720,105 @@ async def get(self): def generate_td( self, TM: dict[str, JSONSerializable], authority: str = None, use_localhost: bool = False ) -> dict[str, JSONSerializable]: - from ...td.forms import Form - TD = copy.deepcopy(TM) # sanitize some things TD["id"] = f"{self.server.router.get_basepath(authority=authority, use_localhost=use_localhost)}/{TD['id']}" - # add forms + + self.add_properties(TD, TM, authority=authority, use_localhost=use_localhost) + self.add_actions(TD, TM, authority=authority, use_localhost=use_localhost) + self.add_events(TD, TM, authority=authority, use_localhost=use_localhost) + + self.add_security_definitions(TD) + return TD + + def add_properties( + self, TD: dict[str, JSONSerializable], TM: dict[str, JSONSerializable], authority: str, use_localhost: bool + ) -> dict[str, JSONSerializable]: for name in TM.get("properties", []): affordance = PropertyAffordance.from_TD(name, TM) href = self.server.router.get_href_for_affordance( affordance, authority=authority, use_localhost=use_localhost ) - if not TD["properties"][name].get("forms", None): - TD["properties"][name]["forms"] = [] + TD["properties"][name]["forms"] = [] http_methods = ( self.server.router.get_target_kwargs_for_affordance(affordance) .get("metadata", {}) .get("http_methods", []) ) for http_method in http_methods: - if affordance.readOnly and http_method.upper() != "GET": - break if http_method.upper() == "DELETE": # currently not in spec although we support it continue - form = Form() + if affordance.readOnly and http_method.upper() != "GET": + break + op = Operations.readproperty if http_method.upper() == "GET" else Operations.writeproperty + form = affordance.retrieve_form(op) + if not form: + form = Form() + form.op = op + form.contentType = Serializers.for_object(TD["id"], TD["title"], affordance.name).content_type form.href = href form.htv_methodName = http_method - form.op = Operations.readproperty if http_method.upper() == "GET" else Operations.writeproperty - form.contentType = "application/json" TD["properties"][name]["forms"].append(form.json()) if affordance.observable: - form = Form() + form = affordance.retrieve_form(Operations.observeproperty) + if not form: + form = Form() + form.contentType = Serializers.for_object(TD["id"], TD["title"], affordance.name).content_type + form.op = Operations.observeproperty form.href = f"{href}/change-event" form.htv_methodName = "GET" - form.contentType = "application/json" - form.op = Operations.observeproperty form.subprotocol = "sse" TD["properties"][name]["forms"].append(form.json()) + + def add_actions( + self, TD: dict[str, JSONSerializable], TM: dict[str, JSONSerializable], authority: str, use_localhost: bool + ) -> dict[str, JSONSerializable]: for name in TM.get("actions", []): affordance = ActionAffordance.from_TD(name, TM) href = self.server.router.get_href_for_affordance( affordance, authority=authority, use_localhost=use_localhost ) - if not TD["actions"][name].get("forms", None): - TD["actions"][name]["forms"] = [] + TD["actions"][name]["forms"] = [] http_methods = ( self.server.router.get_target_kwargs_for_affordance(affordance) .get("metadata", {}) .get("http_methods", []) ) for http_method in http_methods: - form = Form() + form = affordance.retrieve_form(Operations.invokeaction) + if not form: + form = Form() + form.op = Operations.invokeaction + form.contentType = Serializers.for_object(TD["id"], TD["title"], affordance.name).content_type form.href = href form.htv_methodName = http_method - form.op = Operations.invokeaction - form.contentType = "application/json" TD["actions"][name]["forms"].append(form.json()) + + def add_events( + self, TD: dict[str, JSONSerializable], TM: dict[str, JSONSerializable], authority: str, use_localhost: bool + ) -> dict[str, JSONSerializable]: for name in TM.get("events", []): affordance = EventAffordance.from_TD(name, TM) href = self.server.router.get_href_for_affordance( affordance, authority=authority, use_localhost=use_localhost ) - if not TD["events"][name].get("forms", None): - TD["events"][name]["forms"] = [] + TD["events"][name]["forms"] = [] http_methods = ( self.server.router.get_target_kwargs_for_affordance(affordance) .get("metadata", dict(http_methods=["GET"])) .get("http_methods", ["GET"]) ) for http_method in http_methods: - form = Form() + form = affordance.retrieve_form(Operations.subscribeevent) + if not form: + form = Form() + form.op = Operations.subscribeevent + form.contentType = Serializers.for_object(TD["id"], TD["title"], affordance.name).content_type form.href = href form.htv_methodName = http_method - form.op = Operations.subscribeevent - form.contentType = "application/json" form.subprotocol = "sse" TD["events"][name]["forms"].append(form.json()) - self.add_security_definitions(TD) - return TD def add_security_definitions(self, TD: dict[str, JSONSerializable]) -> None: from ...td.security_definitions import SecurityScheme diff --git a/hololinked/td/interaction_affordance.py b/hololinked/td/interaction_affordance.py index 5e474259..167fe343 100644 --- a/hololinked/td/interaction_affordance.py +++ b/hololinked/td/interaction_affordance.py @@ -137,6 +137,30 @@ def retrieve_form(self, op: str, default: typing.Any = None) -> Form: return form return default + def pop_form(self, op: str, default: typing.Any = None) -> Form: + """ + retrieve and remove form for a certain operation, return default if not found + + Parameters + ---------- + op: str + operation for which the form is to be retrieved + default: typing.Any, optional + default value to return if form is not found, by default None. + One can make use of a sensible default value for one's logic. + + Returns + ------- + Dict[str, typing.Any] + JSON representation of the form + """ + if self.forms is None: + return default + for i, form in enumerate(self.forms): + if form.op == op: + return self.forms.pop(i) + return default + @classmethod def generate( cls, interaction: Property | Action | Event, owner: Thing diff --git a/tests/helper-scripts/design_script.py b/tests/helper-scripts/design_script.py index cf522d57..5b79d3d0 100644 --- a/tests/helper-scripts/design_script.py +++ b/tests/helper-scripts/design_script.py @@ -1,9 +1,15 @@ -from hololinked.core import Thing -from hololinked.constants import Operations +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) + from hololinked.server.http import HTTPServer from hololinked.server.zmq import ZMQServer +from hololinked.config import global_config from things import TestThing +global_config.DEBUG = True + thing = TestThing(id="example-test") # thing.run( # access_points=[ @@ -12,7 +18,6 @@ # ] # ) - http_server = HTTPServer(port=9000) zmq_server = ZMQServer(id="example-test-server", things=[thing], access_points="IPC") thing.run(servers=[http_server, zmq_server]) From 68a7e35b754c1e16e7dab23bade51236f9f57246 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 08:47:34 +0200 Subject: [PATCH 03/14] do basic test of content types --- hololinked/serializers/serializers.py | 68 ++--- tests/helper-scripts/design_script.py | 4 + tests/helper-scripts/design_scripty.ipynb | 302 ++++++++++++++-------- tests/test_13_protocols_http.py | 1 + 4 files changed, 236 insertions(+), 139 deletions(-) diff --git a/hololinked/serializers/serializers.py b/hololinked/serializers/serializers.py index 8aa46ed0..c4d419f4 100644 --- a/hololinked/serializers/serializers.py +++ b/hololinked/serializers/serializers.py @@ -379,7 +379,7 @@ class Serializers(metaclass=MappableSingleton): def register(cls, serializer: BaseSerializer, name: str | None = None, override: bool = False) -> None: """ Register a new serializer. It is recommended to implement a content type property/attribute for the serializer - to facilitate automatic deserialization on client side, otherwise deserialization is not gauranteed by this package. + to facilitate automatic deserialization on client side, otherwise deserialization is not gauranteed by this implementation. Parameters ---------- @@ -438,6 +438,39 @@ def for_object(cls, thing_id: str, thing_cls: str, objekt: str) -> BaseSerialize return cls.content_types[cls.object_content_type_map[thing].get(thing, cls.default_content_type)] return cls.default # JSON is default serializer + # @validate_call + @classmethod + def register_for_object(cls, objekt: typing.Any, serializer: BaseSerializer) -> None: + """ + Register (an existing) serializer for a property, action or event. Other option is to register a content type, + the effects are similar. + + Parameters + ---------- + objekt: str | Property | Action | Event + the property, action or event + serializer: BaseSerializer + the serializer to be used + """ + if not isinstance(serializer, BaseSerializer): + raise ValueError("serializer must be an instance of BaseSerializer, given : {}".format(type(serializer))) + from ..core import Property, Action, Event, Thing + + if not isinstance(objekt, (Property, Action, Event)) and not issubklass(objekt, Thing): + raise ValueError("object must be a Property, Action or Event, or Thing, got : {}".format(type(objekt))) + if issubklass(objekt, Thing): + owner = objekt.__name__ + elif not objekt.owner: + raise ValueError("object owner cannot be determined : {}".format(objekt)) + else: + owner = objekt.owner.__name__ + if owner not in cls.object_serializer_map: + cls.object_serializer_map[owner] = dict() + if issubklass(objekt, Thing): + cls.object_serializer_map[owner][objekt.__name__] = serializer + else: + cls.object_serializer_map[owner][objekt.name] = serializer + # @validate_call @classmethod def register_content_type_for_object(cls, objekt: typing.Any, content_type: str) -> None: @@ -523,39 +556,6 @@ def register_content_type_for_thing_instance(cls, thing_id: str, content_type: s cls.object_content_type_map[thing_id][thing_id] = content_type # remember, its a redundant key, TODO - # @validate_call - @classmethod - def register_for_object(cls, objekt: typing.Any, serializer: BaseSerializer) -> None: - """ - Register (an existing) serializer for a property, action or event. Other option is to register a content type, - the effects are similar. - - Parameters - ---------- - objekt: str | Property | Action | Event - the property, action or event - serializer: BaseSerializer - the serializer to be used - """ - if not isinstance(serializer, BaseSerializer): - raise ValueError("serializer must be an instance of BaseSerializer, given : {}".format(type(serializer))) - from ..core import Property, Action, Event, Thing - - if not isinstance(objekt, (Property, Action, Event)) and not issubklass(objekt, Thing): - raise ValueError("object must be a Property, Action or Event, or Thing, got : {}".format(type(objekt))) - if issubklass(objekt, Thing): - owner = objekt.__name__ - elif not objekt.owner: - raise ValueError("object owner cannot be determined : {}".format(objekt)) - else: - owner = objekt.owner.__name__ - if owner not in cls.object_serializer_map: - cls.object_serializer_map[owner] = dict() - if issubklass(objekt, Thing): - cls.object_serializer_map[owner][objekt.__name__] = serializer - else: - cls.object_serializer_map[owner][objekt.name] = serializer - @classmethod def register_for_object_per_thing_instance(cls, thing_id: str, objekt: str, serializer: BaseSerializer) -> None: """ diff --git a/tests/helper-scripts/design_script.py b/tests/helper-scripts/design_script.py index 5b79d3d0..c25ef92c 100644 --- a/tests/helper-scripts/design_script.py +++ b/tests/helper-scripts/design_script.py @@ -5,12 +5,16 @@ from hololinked.server.http import HTTPServer from hololinked.server.zmq import ZMQServer +from hololinked.serializers import Serializers from hololinked.config import global_config from things import TestThing global_config.DEBUG = True thing = TestThing(id="example-test") + +Serializers.register_for_object(TestThing.db_init_int_prop, Serializers.pickle) +Serializers.register_for_object(TestThing.set_non_remote_number_prop, Serializers.msgpack) # thing.run( # access_points=[ # ("ZMQ", "IPC"), diff --git a/tests/helper-scripts/design_scripty.ipynb b/tests/helper-scripts/design_scripty.ipynb index b5624172..76bd8432 100644 --- a/tests/helper-scripts/design_scripty.ipynb +++ b/tests/helper-scripts/design_scripty.ipynb @@ -2,12 +2,17 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "id": "c5ea7132", "metadata": {}, "outputs": [], "source": [ "import importlib\n", + "import os\n", + "import sys\n", + "\n", + "sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), \"../\")))\n", + "\n", "import hololinked\n", "import things \n", "\n", @@ -2169,64 +2174,60 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "b67beb49", "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO - 2025-09-21T09:39:11:186 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - created socket type: ROUTER with address: tcp://localhost:5558 & identity: example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046|sync and connected\n", - "INFO - 2025-09-21T09:39:11:188 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:39:11:703 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - got no response for handshake\n", - "INFO - 2025-09-21T09:39:11:704 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:39:11:705 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - client 'example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046|sync' handshook with server 'example-test'\n", - "INFO - 2025-09-21T09:39:11:710 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - created socket type: ROUTER with address: tcp://localhost:5558 & identity: example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046|async and connected\n" - ] - }, { "data": { "text/plain": [ "{'context': ['https://www.w3.org/2022/wot/td/v1.1'],\n", - " 'id': 'example-test',\n", + " 'id': 'http://127.0.0.1:9000/example-test',\n", " 'title': 'TestThing',\n", " 'properties': {'db_commit_number_prop': {'description': 'A fully editable number property to check commits to db on write operations',\n", " 'default': 0,\n", " 'type': 'number',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/db-commit-number-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/db-commit-number-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'db_init_int_prop': {'description': 'An integer property to check initialization from db',\n", " 'default': 1,\n", " 'type': 'integer',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/db-init-int-prop',\n", " 'op': 'readproperty',\n", - " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/x-pickle'},\n", + " {'href': 'http://127.0.0.1:9000/example-test/db-init-int-prop',\n", " 'op': 'writeproperty',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'PUT',\n", + " 'contentType': 'application/x-pickle'}]},\n", " 'db_persist_selector_prop': {'description': 'A selector property to check persistence to db on write operations',\n", " 'default': 'a',\n", " 'oneOf': [{'type': 'string'}, {'type': 'integer'}],\n", " 'enum': ['a', 'b', 'c', 1],\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/db-persist-selector-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/db-persist-selector-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'deletable_class_prop': {'description': 'deletable class property with custom deleter',\n", " 'default': 100,\n", " 'type': 'number',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/deletable-class-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/deletable-class-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'int_prop': {'description': 'An integer property with step and bounds constraints to check RW',\n", " 'default': 5,\n", @@ -2234,11 +2235,13 @@ " 'minimum': 0,\n", " 'maximum': 100,\n", " 'multipleOf': 2,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/int-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/int-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'json_schema_prop': {'description': 'A property with a json schema to check RW',\n", " 'oneOf': [{'type': 'null'},\n", @@ -2246,62 +2249,77 @@ " 'minLength': 1,\n", " 'maxLength': 10,\n", " 'pattern': '^[a-z]+$'}],\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/json-schema-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/json-schema-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'managed_class_prop': {'description': '(managed) class property with custom getter/setter',\n", " 'default': 0.0,\n", " 'type': 'number',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/managed-class-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/managed-class-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'not_a_class_prop': {'description': 'test property with class_member=False',\n", " 'default': 43,\n", " 'type': 'number',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/not-a-class-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/not-a-class-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'number_prop': {'description': 'A fully editable number property',\n", " 'default': 1,\n", " 'type': 'number',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/number-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/number-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'observable_list_prop': {'description': 'An observable list property to check observable events on write operations',\n", " 'oneOf': [{'type': 'array'}, {'type': 'null'}],\n", " 'observable': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/observable-list-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/observable-list-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " {'href': 'http://127.0.0.1:9000/example-test/observable-list-prop/change-event',\n", " 'op': 'observeproperty',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'observable_readonly_prop': {'description': 'An observable readonly property to check observable events on read operations',\n", " 'default': 0,\n", " 'readOnly': True,\n", " 'type': 'number',\n", " 'observable': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/observable-readonly-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " {'href': 'http://127.0.0.1:9000/example-test/observable-readonly-prop/change-event',\n", " 'op': 'observeproperty',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'pydantic_prop': {'description': 'A property with a pydantic model to check RW',\n", " 'oneOf': [{'type': 'null'},\n", " {'properties': {'foo': {'type': 'string'},\n", @@ -2309,80 +2327,97 @@ " 'foo_bar': {'type': 'number'}},\n", " 'required': ['foo', 'bar', 'foo_bar'],\n", " 'type': 'object'}],\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/pydantic-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/pydantic-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'pydantic_simple_prop': {'description': 'A property with a simple pydantic model to check RW',\n", " 'oneOf': [{'type': 'null'}, {'type': 'integer'}],\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/pydantic-simple-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/pydantic-simple-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'readonly_class_prop': {'description': 'read-only class property',\n", " 'default': '',\n", " 'readOnly': True,\n", " 'type': 'string',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/readonly-class-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'}]},\n", " 'selector_prop': {'description': 'A selector property to check RW',\n", " 'default': 'a',\n", " 'oneOf': [{'type': 'string'}, {'type': 'integer'}],\n", " 'enum': ['a', 'b', 'c', 1],\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/selector-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/selector-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'simple_class_prop': {'description': 'simple class property with default value',\n", " 'default': 42,\n", " 'type': 'number',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/simple-class-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/simple-class-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'sleeping_prop': {'description': 'A property that sleeps for 10 seconds on read operations',\n", " 'default': 0,\n", " 'readOnly': True,\n", " 'type': 'number',\n", " 'observable': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/sleeping-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " {'href': 'http://127.0.0.1:9000/example-test/sleeping-prop/change-event',\n", " 'op': 'observeproperty',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'string_prop': {'description': 'A string property with a regex constraint to check value errors',\n", " 'default': 'hello',\n", " 'type': 'string',\n", " 'pattern': '^[a-z]+',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/string-prop',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/string-prop',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", " 'total_number_of_events': {'description': 'Total number of events pushed',\n", " 'default': 100,\n", " 'type': 'number',\n", " 'minimum': 1,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/total-number-of-events',\n", " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", " 'contentType': 'application/json'},\n", - " {'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " {'href': 'http://127.0.0.1:9000/example-test/total-number-of-events',\n", " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]}},\n", " 'actions': {'action_echo': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/action-echo',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'execute_instruction': {'description': \"executes instruction given by the ASCII string parameter 'command'. If return data size is greater than 0, it reads the response and returns the response. Return Data Size - in bytes - 1 ASCII character = 1 Byte.\",\n", " 'input': {'properties': {'command': {'type': 'string'},\n", @@ -2391,13 +2426,15 @@ " 'type': 'object'},\n", " 'output': {'type': 'string'},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/execute-instruction',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", - " 'exit': {'description': 'Stop serving the object. This method can only be called remotely',\n", + " 'exit': {'description': 'Stop serving the object. This method usually needs to be called remotely',\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/exit',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'get_analogue_offset': {'description': 'analogue offset for a voltage range and coupling',\n", " 'input': {'type': 'object',\n", @@ -2421,20 +2458,24 @@ " 'maxItems': 2,\n", " 'items': {'type': 'number'}},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/get-analogue-offset',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'get_mixed_content_data': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/get-mixed-content-data',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'get_non_remote_number_prop': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/get-non-remote-number-prop',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'get_serialized_data': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/get-serialized-data',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'get_thing_model': {'description': \"generate the [Thing Model](https://www.w3.org/TR/wot-thing-description11/#introduction-tm) of the object. The model is a JSON that describes the object's properties, actions, events and their metadata, without the protocol information. The model can be used by a client to understand the object's capabilities.\",\n", " 'input': {'properties': {'ignore_errors': {'default': False,\n", @@ -2444,29 +2485,34 @@ " 'type': 'array'}},\n", " 'type': 'object'},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/resources/wot-tm',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'get_transports': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/get-transports',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", - " 'ping': {'description': 'ping the `Thing` to see if it is alive. Ping successful when action succeeds with no return value and no timeout or exception raised on the client side.',\n", + " 'ping': {'description': 'ping to see if it is alive. Successful when action succeeds with no return value and no timeout or exception raised on the client side.',\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/ping',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'print_props': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/print-props',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'push_events': {'input': {'properties': {'event_name': {'default': 'test_event',\n", " 'type': 'string'},\n", " 'total_number_of_events': {'default': 100, 'type': 'integer'}},\n", " 'type': 'object'},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/push-events',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'set_channel': {'description': 'Set the parameter for a channel. https://www.picotech.com/download/manuals/picoscope-6000-series-a-api-programmers-guide.pdf',\n", " 'input': {'type': 'object',\n", @@ -2490,8 +2536,9 @@ " 'coupling': {'type': 'string', 'enum': ['AC', 'DC']},\n", " 'bw_limiter': {'type': 'string', 'enum': ['full', '20MHz']}}},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/set-channel',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'set_channel_pydantic': {'description': 'Set the parameter for a channel. https://www.picotech.com/download/manuals/picoscope-6000-series-a-api-programmers-guide.pdf',\n", " 'input': {'properties': {'channel': {'enum': ['A', 'B', 'C', 'D'],\n", @@ -2520,18 +2567,21 @@ " 'required': ['channel'],\n", " 'type': 'object'},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/set-channel-pydantic',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'set_non_remote_number_prop': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/set-non-remote-number-prop',\n", " 'op': 'invokeaction',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'POST',\n", + " 'contentType': 'application/msgpack'}]},\n", " 'set_sensor_model': {'description': 'Set the attached sensor to the meter under control. Sensor should be defined as a class and added to the AllowedSensors dict.',\n", " 'input': {'type': 'string', 'enum': ['QE25LP-S-MB', 'QE12LP-S-MB-QED-D0']},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/set-sensor-model',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'set_sensor_model_pydantic': {'description': 'Set the attached sensor to the meter under control. Sensor should be defined as a class and added to the AllowedSensors dict.',\n", " 'input': {'properties': {'value': {'enum': ['QE25LP-S-MB',\n", @@ -2540,12 +2590,14 @@ " 'required': ['value'],\n", " 'type': 'object'},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/set-sensor-model-pydantic',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'sleep': {'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/sleep',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", " 'start_acquisition': {'description': 'Start acquisition of energy measurements.',\n", " 'input': {'properties': {'max_count': {'exclusiveMinimum': 0,\n", @@ -2553,57 +2605,65 @@ " 'required': ['max_count'],\n", " 'type': 'object'},\n", " 'synchronous': True,\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:5558',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/start-acquisition',\n", " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]}},\n", " 'events': {'data_point_event': {'description': 'Event raised when a new data point is available',\n", " 'data': {'type': 'object',\n", " 'properties': {'timestamp': {'type': 'string'},\n", " 'energy': {'type': 'number'}},\n", " 'required': ['timestamp', 'energy']},\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/data-point-event',\n", " 'op': 'subscribeevent',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'test_binary_payload_event': {'description': 'test event with binary payload',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/test-binary-payload-event',\n", " 'op': 'subscribeevent',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'test_event': {'description': 'test event with arbitrary payload',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/test-event',\n", " 'op': 'subscribeevent',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'test_event_with_json_schema': {'description': 'test event with schema validation',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/test-event-with-json-schema',\n", " 'op': 'subscribeevent',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'test_event_with_pydantic_schema': {'description': 'test event with pydantic schema validation',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/test-event-with-pydantic-schema',\n", " 'op': 'subscribeevent',\n", - " 'contentType': 'application/json'}]},\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]},\n", " 'test_mixed_content_payload_event': {'description': 'test event with mixed content payload',\n", - " 'forms': [{'href': 'tcp://LAPTOP-TUBBGPB8:60000',\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/test-mixed-content-payload-event',\n", " 'op': 'subscribeevent',\n", - " 'contentType': 'application/json'}]}}}" + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/json',\n", + " 'subprotocol': 'sse'}]}},\n", + " 'securityDefinitions': {'nosec': {'scheme': 'nosec',\n", + " 'description': 'currently no security scheme supported - use cookie auth directly on hololinked.server.HTTPServer object'}},\n", + " 'security': ['nosec']}" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO - 2025-09-21T09:39:11:745 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:39:11:746 - example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046 - client 'example-test|example-test|tcp://localhost:5558|2f1ecdef-432d-42af-bfe6-03496ab70046|async' handshook with server 'example-test'\n" - ] } ], "source": [ "from hololinked.client import ClientFactory\n", "\n", - "# object_proxy = ClientFactory.http(\"http://localhost:8080/example-test/resources/wot-td\", ignore_TD_errors=True)\n", - "object_proxy = ClientFactory.zmq(server_id=\"example-test\", thing_id=\"example-test\", access_point=\"tcp://localhost:5558\", ignore_TD_errors=True)\n", + "object_proxy = ClientFactory.http(\"http://localhost:9000/example-test/resources/wot-td\", ignore_TD_errors=True)\n", + "# object_proxy = ClientFactory.zmq(server_id=\"example-test\", thing_id=\"example-test\", access_point=\"tcp://localhost:5558\", ignore_TD_errors=True)\n", "object_proxy.td" ] }, @@ -2874,7 +2934,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 5, "id": "3285efde", "metadata": {}, "outputs": [ @@ -2897,9 +2957,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "169fd06e", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "object_proxy.read_property(\"db_init_int_prop\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41595859", + "metadata": {}, + "outputs": [], + "source": [ + "object_proxy.set_non_remote_number_prop(5)\n", + "object_proxy.get_non_remote_number_prop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "113441d1", + "metadata": {}, "outputs": [], "source": [] } diff --git a/tests/test_13_protocols_http.py b/tests/test_13_protocols_http.py index b2bb4f65..7a9987aa 100644 --- a/tests/test_13_protocols_http.py +++ b/tests/test_13_protocols_http.py @@ -648,6 +648,7 @@ def wait_until_server_ready(cls, port, tries: int = 10): try: response = session.get(f"http://127.0.0.1:{port}/liveness") if response.status_code in [200, 201, 202, 204]: + time.sleep(2) return except Exception: pass From 37d7084dd2a6de63c68a47fe6574ac0035b5f420 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 08:50:36 +0200 Subject: [PATCH 04/14] update changelog and readme --- CHANGELOG.md | 4 ++++ README.md | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5cdfa21..17f26d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ✓ means ready to try +## [v0.3.4] - 2025-10-02 + +- fixes a bug in content type in the forms of TD for HTTP protocol binding, when multiple serializers are used + ## [v0.3.3] - 2025-09-25 - updates API reference largely to latest version diff --git a/README.md b/README.md index 93961303..29436587 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,10 @@ if __name__ == '__main__': id='spectrometer', serial_number='S14155', ).run( - access_points=['HTTP', 'ZMQ-IPC'] + access_points=[ + ("ZMQ", "IPC"), + ("HTTP", 8080), + ] ) # HTTP & ZMQ Interprocess Communication ``` From 0f5dc99d8520b736b0d1cde0f2cf687862494bf2 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 09:49:04 +0200 Subject: [PATCH 05/14] implement __deepcopy__ and __getstate__ --- hololinked/td/interaction_affordance.py | 69 +++++++++++++++++-------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/hololinked/td/interaction_affordance.py b/hololinked/td/interaction_affordance.py index 167fe343..d7521474 100644 --- a/hololinked/td/interaction_affordance.py +++ b/hololinked/td/interaction_affordance.py @@ -1,4 +1,5 @@ import typing +import copy from enum import Enum from typing import ClassVar, Optional from pydantic import ConfigDict @@ -239,6 +240,29 @@ def register_descriptor( ) InteractionAffordance._custom_schema_generators[descriptor] = schema_generator + def build_non_compliant_metadata(self) -> None: + """ + If by chance, there is additional non standard metadata to be added (i.e. also that is outside LD, or + may not even be within the TD), they can be added here. + """ + pass + + def override_defaults(self, **kwargs): + """ + Override default values with provided keyword arguments, especially thing_id, owner name, object name etc. + """ + for key, value in kwargs.items(): + if key == "name": + self._name = value + elif key == "thing_id": + self._thing_id = value + elif key == "owner": + self._owner = value + elif key == "thing_cls": + self._thing_cls = value + elif hasattr(self, key) or key in self.model_fields: + setattr(self, key, value) + def __hash__(self): return hash(self.thing_id + "" if not self.thing_cls else self.thing_cls.__name__ + self.name) @@ -262,28 +286,29 @@ def __eq__(self, value): return False return self.thing_id == value.thing_id and self.name == value.name - def build_non_compliant_metadata(self) -> None: - """ - If by chance, there is additional non standard metadata to be added (i.e. also that is outside LD, or - may not even be within the TD), they can be added here. - """ - pass - - def override_defaults(self, **kwargs): - """ - Override default values with provided keyword arguments, especially thing_id, owner name, object name etc. - """ - for key, value in kwargs.items(): - if key == "name": - self._name = value - elif key == "thing_id": - self._thing_id = value - elif key == "owner": - self._owner = value - elif key == "thing_cls": - self._thing_cls = value - elif hasattr(self, key) or key in self.model_fields: - setattr(self, key, value) + def __deepcopy__(self, memo): + if self.__class__ == PropertyAffordance: + result = PropertyAffordance() + elif self.__class__ == ActionAffordance: + result = ActionAffordance() + elif self.__class__ == EventAffordance: + result = EventAffordance() + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k not in ("_owner", "_thing_cls", "_objekt"): + setattr(result, k, copy.deepcopy(v, memo)) + return result + + def __getstate__(self): + state = self.__dict__.copy() + # Remove possibly unpicklable entries + if "_owner" in state: + del state["_owner"] + if "_thing_cls" in state: + del state["_thing_cls"] + if "_objekt" in state: + del state["_objekt"] + return state class PropertyAffordance(DataSchema, InteractionAffordance): From 15f63f1c8ba87bab1c33bb194572ec7157ad4992 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 09:49:29 +0200 Subject: [PATCH 06/14] codestyle in helper scripts --- tests/helper-scripts/design_script.py | 1 + tests/helper-scripts/design_scripty.ipynb | 113 ++++++++- tests/things/spectrometer.py | 285 ++++++++++++---------- tests/things/starter.py | 4 +- tests/things/test_thing.py | 167 ++++--------- 5 files changed, 310 insertions(+), 260 deletions(-) diff --git a/tests/helper-scripts/design_script.py b/tests/helper-scripts/design_script.py index c25ef92c..51625ad5 100644 --- a/tests/helper-scripts/design_script.py +++ b/tests/helper-scripts/design_script.py @@ -15,6 +15,7 @@ Serializers.register_for_object(TestThing.db_init_int_prop, Serializers.pickle) Serializers.register_for_object(TestThing.set_non_remote_number_prop, Serializers.msgpack) +Serializers.register_for_object(TestThing.get_non_remote_number_prop, Serializers.msgpack) # thing.run( # access_points=[ # ("ZMQ", "IPC"), diff --git a/tests/helper-scripts/design_scripty.ipynb b/tests/helper-scripts/design_scripty.ipynb index 76bd8432..ce696bff 100644 --- a/tests/helper-scripts/design_scripty.ipynb +++ b/tests/helper-scripts/design_scripty.ipynb @@ -2174,10 +2174,20 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 18, "id": "b67beb49", "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO - 2025-10-04T09:04:47:988 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|sync and connected\n", + "INFO - 2025-10-04T09:04:47:990 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T09:04:47:991 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - client 'example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|sync' handshook with server 'example-test-server'\n", + "INFO - 2025-10-04T09:04:47:997 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|async and connected\n" + ] + }, { "data": { "text/plain": [ @@ -2471,7 +2481,7 @@ " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/get-non-remote-number-prop',\n", " 'op': 'invokeaction',\n", " 'htv:methodName': 'POST',\n", - " 'contentType': 'application/json'}]},\n", + " 'contentType': 'application/msgpack'}]},\n", " 'get_serialized_data': {'synchronous': True,\n", " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/get-serialized-data',\n", " 'op': 'invokeaction',\n", @@ -2654,16 +2664,24 @@ " 'security': ['nosec']}" ] }, - "execution_count": 4, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO - 2025-10-04T09:04:48:029 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T09:04:48:030 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - client 'example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|async' handshook with server 'example-test-server'\n" + ] } ], "source": [ "from hololinked.client import ClientFactory\n", "\n", "object_proxy = ClientFactory.http(\"http://localhost:9000/example-test/resources/wot-td\", ignore_TD_errors=True)\n", - "# object_proxy = ClientFactory.zmq(server_id=\"example-test\", thing_id=\"example-test\", access_point=\"tcp://localhost:5558\", ignore_TD_errors=True)\n", + "object_proxy_zmq = ClientFactory.zmq(server_id=\"example-test-server\", thing_id=\"example-test\", access_point=\"IPC\", ignore_TD_errors=True)\n", "object_proxy.td" ] }, @@ -2957,7 +2975,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "id": "169fd06e", "metadata": {}, "outputs": [ @@ -2967,7 +2985,7 @@ "1" ] }, - "execution_count": 6, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -2978,20 +2996,95 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "41595859", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "15" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "object_proxy.set_non_remote_number_prop(5)\n", + "object_proxy.set_non_remote_number_prop(15)\n", "object_proxy.get_non_remote_number_prop()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "113441d1", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['INPROC', 'IPC']" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "object_proxy.get_transports()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "ea4ed954", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "b'foobar'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# object_proxy.get_serialized_data()\n", + "object_proxy_zmq.get_serialized_data()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "0077604d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "foobar\n", + "b'foobar'\n" + ] + } + ], + "source": [ + "val, binary_val = object_proxy_zmq.get_mixed_content_data()\n", + "print(val)\n", + "print(binary_val)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a9a4f24", + "metadata": {}, "outputs": [], "source": [] } diff --git a/tests/things/spectrometer.py b/tests/things/spectrometer.py index 580f80d8..735ab001 100644 --- a/tests/things/spectrometer.py +++ b/tests/things/spectrometer.py @@ -8,44 +8,38 @@ from hololinked.core import Thing, Property, action, Event -from hololinked.core.properties import (String, Integer, Number, List, Boolean, - Selector, ClassSelector, TypedList) +from hololinked.core.properties import String, Integer, Number, List, Boolean, Selector, ClassSelector, TypedList from hololinked.core.state_machine import StateMachine from hololinked.serializers import JSONSerializer from hololinked.schema_validators import JSONSchema from hololinked.server.http import HTTPServer -@dataclass +@dataclass class Intensity: - value : numpy.ndarray - timestamp : str + value: numpy.ndarray + timestamp: str schema = { - "type" : "object", - "properties" : { - "value" : { - "type" : "array", - "items" : { - "type" : "number" - }, + "type": "object", + "properties": { + "value": { + "type": "array", + "items": {"type": "number"}, }, - "timestamp" : { - "type" : "string" - } - } + "timestamp": {"type": "string"}, + }, } @property def not_completely_black(self): - if any(self.value[i] > 0 for i in range(len(self.value))): - return True + if any(self.value[i] > 0 for i in range(len(self.value))): + return True return False - -JSONSerializer.register_type_replacement(numpy.ndarray, lambda obj : obj.tolist()) -JSONSchema.register_type_replacement(Intensity, 'object', Intensity.schema) +JSONSerializer.register_type_replacement(numpy.ndarray, lambda obj: obj.tolist()) +JSONSchema.register_type_replacement(Intensity, "object", Intensity.schema) connect_args = { @@ -53,13 +47,12 @@ def not_completely_black(self): "properties": { "serial_number": {"type": "string"}, "trigger_mode": {"type": "integer"}, - "integration_time": {"type": "number"} + "integration_time": {"type": "number"}, }, - "additionalProperties": False + "additionalProperties": False, } - class States(StrEnum): DISCONNECTED = "DISCONNECTED" ON = "ON" @@ -75,45 +68,47 @@ class OceanOpticsSpectrometer(Thing): states = States - status = String(readonly=True, fget=lambda self: self._status, - doc="descriptive status of current operation") # type: str + status = String(readonly=True, fget=lambda self: self._status, doc="descriptive status of current operation") # type: str + + serial_number = String( + default=None, allow_None=True, doc="serial number of the spectrometer to connect/or connected" + ) # type: str - serial_number = String(default=None, allow_None=True, - doc="serial number of the spectrometer to connect/or connected")# type: str + last_intensity = ClassSelector( + default=None, allow_None=True, class_=Intensity, doc="last measurement intensity (in arbitrary units)" + ) # type: Intensity - last_intensity = ClassSelector(default=None, allow_None=True, class_=Intensity, - doc="last measurement intensity (in arbitrary units)") # type: Intensity - intensity_measurement_event = Event( - doc="event generated on measurement of intensity, max 30 per second even if measurement is faster.", - schema=Intensity.schema) - - reference_intensity = ClassSelector(default=None, allow_None=True, class_=Intensity, - doc="reference intensity to overlap in background") # type: Intensity - - + doc="event generated on measurement of intensity, max 30 per second even if measurement is faster.", + schema=Intensity.schema, + ) + + reference_intensity = ClassSelector( + default=None, allow_None=True, class_=Intensity, doc="reference intensity to overlap in background" + ) # type: Intensity + def __init__(self, id: str, serial_number: typing.Optional[str] = None, **kwargs) -> None: super().__init__(id=id, serial_number=serial_number, **kwargs) self.set_status("disconnected") if serial_number is not None: self.connect() - self._acquisition_thread = None + self._acquisition_thread = None self._running = False - + def set_status(self, *args) -> None: if len(args) == 1: self._status = args[0] else: - self._status = ' '.join(args) - + self._status = " ".join(args) + @action(input_schema=connect_args) - def connect(self, serial_number : str = None, trigger_mode : int = None, integration_time : float = None) -> None: + def connect(self, serial_number: str = None, trigger_mode: int = None, integration_time: float = None) -> None: if serial_number is not None: self.serial_number = serial_number self.state_machine.current_state = self.states.ON self._pixel_count = 50 self._wavelengths = [i for i in range(self._pixel_count)] - self._model = 'STS' + self._model = "STS" self._max_intensity = 16384 if trigger_mode is not None: self.trigger_mode = trigger_mode @@ -128,144 +123,164 @@ def connect(self, serial_number : str = None, trigger_mode : int = None, integra self.logger.debug(f"opened device with serial number {self.serial_number} with model {self.model}") self.set_status("ready to start acquisition") - model = String(default=None, allow_None=True, readonly=True, - doc="model of the connected spectrometer", - fget=lambda self: self._model if self.state_machine.current_state != self.states.DISCONNECTED else None - ) # type: str - - wavelengths = List(default=[], item_type=(float, int), readonly=True, allow_None=False, - # this is only for testing, be careful - doc="wavelength bins of measurement", - fget=lambda self: self._wavelengths if self.state_machine.current_state != self.states.DISCONNECTED else None, - ) # type: typing.List[typing.Union[float, int]] - - pixel_count = Integer(default=None, allow_None=True, readonly=True, - doc="number of points in wavelength", - fget=lambda self: self._pixel_count if self.state_machine.current_state != self.states.DISCONNECTED else None - ) # type: int - - max_intensity = Number(readonly=True, - doc="""the maximum intensity that can be returned by the spectrometer in (a.u.). + model = String( + default=None, + allow_None=True, + readonly=True, + doc="model of the connected spectrometer", + fget=lambda self: self._model if self.state_machine.current_state != self.states.DISCONNECTED else None, + ) # type: str + + wavelengths = List( + default=[], + item_type=(float, int), + readonly=True, + allow_None=False, + # this is only for testing, be careful + doc="wavelength bins of measurement", + fget=lambda self: self._wavelengths if self.state_machine.current_state != self.states.DISCONNECTED else None, + ) # type: typing.List[typing.Union[float, int]] + + pixel_count = Integer( + default=None, + allow_None=True, + readonly=True, + doc="number of points in wavelength", + fget=lambda self: self._pixel_count if self.state_machine.current_state != self.states.DISCONNECTED else None, + ) # type: int + + max_intensity = Number( + readonly=True, + doc="""the maximum intensity that can be returned by the spectrometer in (a.u.). It's possible that the spectrometer saturates already at lower values.""", - fget=lambda self: self._max_intensity if self.state_machine.current_state != self.states.DISCONNECTED else None - ) # type: float - + fget=lambda self: self._max_intensity if self.state_machine.current_state != self.states.DISCONNECTED else None, + ) # type: float + @action() def disconnect(self): self.state_machine.current_state = self.states.DISCONNECTED - trigger_mode = Selector(objects=[0, 1, 2, 3, 4], default=0, observable=True, - doc="""0 = normal/free running, 1 = Software trigger, 2 = Ext. Trigger Level, - 3 = Ext. Trigger Synchro/ Shutter mode, 4 = Ext. Trigger Edge""") # type: int - - @trigger_mode.setter - def apply_trigger_mode(self, value : int): - self._trigger_mode = value - - @trigger_mode.getter + trigger_mode = Selector( + objects=[0, 1, 2, 3, 4], + default=0, + observable=True, + doc="""0 = normal/free running, 1 = Software trigger, 2 = Ext. Trigger Level, + 3 = Ext. Trigger Synchro/ Shutter mode, 4 = Ext. Trigger Edge""", + ) # type: int + + @trigger_mode.setter + def apply_trigger_mode(self, value: int): + self._trigger_mode = value + + @trigger_mode.getter def get_trigger_mode(self): try: return self._trigger_mode except: - return OceanOpticsSpectrometer.properties["trigger_mode"].default - - - integration_time = Number(default=1000, bounds=(0.001, None), crop_to_bounds=True, observable=True, - doc="integration time of measurement in milliseconds") # type: float - - @integration_time.setter - def apply_integration_time(self, value : float): - self._integration_time = int(value) - - @integration_time.getter + return OceanOpticsSpectrometer.properties["trigger_mode"].default + + integration_time = Number( + default=1000, + bounds=(0.001, None), + crop_to_bounds=True, + observable=True, + doc="integration time of measurement in milliseconds", + ) # type: float + + @integration_time.setter + def apply_integration_time(self, value: float): + self._integration_time = int(value) + + @integration_time.getter def get_integration_time(self) -> float: try: return self._integration_time except: - return OceanOpticsSpectrometer.properties["integration_time"].default - - background_correction = Selector(objects=['AUTO', 'CUSTOM', None], default=None, allow_None=True, - doc="set True for Seabreeze internal black level correction") # type: typing.Optional[str] - - custom_background_intensity = TypedList(item_type=(float, int)) # type: typing.List[typing.Union[float, int]] - - nonlinearity_correction = Boolean(default=False, - doc="automatic correction of non linearity in detector CCD") # type: bool + return OceanOpticsSpectrometer.properties["integration_time"].default + + background_correction = Selector( + objects=["AUTO", "CUSTOM", None], + default=None, + allow_None=True, + doc="set True for Seabreeze internal black level correction", + ) # type: typing.Optional[str] + + custom_background_intensity = TypedList(item_type=(float, int)) # type: typing.List[typing.Union[float, int]] + + nonlinearity_correction = Boolean(default=False, doc="automatic correction of non linearity in detector CCD") # type: bool @action() def start_acquisition(self) -> None: - self.stop_acquisition() # Just a shield - self._acquisition_thread = threading.Thread(target=self.measure) + self.stop_acquisition() # Just a shield + self._acquisition_thread = threading.Thread(target=self.measure) self._acquisition_thread.start() @action() def stop_acquisition(self) -> None: if self._acquisition_thread is not None: self.logger.debug(f"stopping acquisition thread with thread-ID {self._acquisition_thread.ident}") - self._running = False # break infinite loop + self._running = False # break infinite loop # Reduce the measurement that will proceed in new trigger mode to 1ms self._acquisition_thread.join() - self._acquisition_thread = None + self._acquisition_thread = None # re-apply old values self.trigger_mode = self.trigger_mode - self.integration_time = self.integration_time - + self.integration_time = self.integration_time - def measure(self, max_count = None): + def measure(self, max_count=None): try: self._running = True self.state_machine.current_state = self.states.MEASURING self.set_status("measuring") - self.logger.info(f'starting continuous acquisition loop with trigger mode {self.trigger_mode} & integration time {self.integration_time} in thread with ID {threading.get_ident()}') + self.logger.info( + f"starting continuous acquisition loop with trigger mode {self.trigger_mode} & integration time {self.integration_time} in thread with ID {threading.get_ident()}" + ) loop = 0 while self._running: if max_count is not None and loop > max_count: - break - loop += 1 + break + loop += 1 time.sleep(self.integration_time / 1000.0) # simulate integration time # Following is a blocking command - self.spec.intensities - self.logger.debug(f'starting measurement count {loop}') + self.logger.debug(f"starting measurement count {loop}") _current_intensity = [numpy.random.randint(0, self.max_intensity) for i in range(self._pixel_count)] - if self.background_correction == 'CUSTOM': + if self.background_correction == "CUSTOM": if self.custom_background_intensity is None: - self.logger.warning('no background correction possible') + self.logger.warning("no background correction possible") self.state_machine.set_state(self.states.ALARM) else: _current_intensity = _current_intensity - self.custom_background_intensity curtime = datetime.datetime.now() - timestamp = curtime.strftime('%d.%m.%Y %H:%M:%S.') + '{:03d}'.format(int(curtime.microsecond /1000)) - self.logger.debug(f'measurement taken at {timestamp} - measurement count {loop}') + timestamp = curtime.strftime("%d.%m.%Y %H:%M:%S.") + "{:03d}".format(int(curtime.microsecond / 1000)) + self.logger.debug(f"measurement taken at {timestamp} - measurement count {loop}") if self._running: - # To stop the acquisition in hardware trigger mode, we set running to False in stop_acquisition() - # and then change the trigger mode for self.spec.intensities to unblock. This exits this - # infintie loop. Therefore, to know, whether self.spec.intensities finished, whether due to trigger - # mode or due to actual completion of measurement, we check again if self._running is True. - self.last_intensity = Intensity( - value=_current_intensity, - timestamp=timestamp - ) - if self.last_intensity.not_completely_black: + # To stop the acquisition in hardware trigger mode, we set running to False in stop_acquisition() + # and then change the trigger mode for self.spec.intensities to unblock. This exits this + # infintie loop. Therefore, to know, whether self.spec.intensities finished, whether due to trigger + # mode or due to actual completion of measurement, we check again if self._running is True. + self.last_intensity = Intensity(value=_current_intensity, timestamp=timestamp) + if self.last_intensity.not_completely_black: self.intensity_measurement_event.push(self.last_intensity) self.state_machine.current_state = self.states.MEASURING else: - self.logger.warning('trigger delayed or no trigger or erroneous data - completely black') + self.logger.warning("trigger delayed or no trigger or erroneous data - completely black") self.state_machine.current_state = self.states.ALARM - if self.state_machine.current_state not in [self.states.FAULT, self.states.ALARM]: + if self.state_machine.current_state not in [self.states.FAULT, self.states.ALARM]: self.state_machine.current_state = self.states.ON self.set_status("ready to start acquisition") - self.logger.info("ending continuous acquisition") - self._running = False + self.logger.info("ending continuous acquisition") + self._running = False except Exception as ex: self.logger.error(f"error during acquisition - {str(ex)}, {type(ex)}") - self.set_status(f'error during acquisition - {str(ex)}, {type(ex)}') + self.set_status(f"error during acquisition - {str(ex)}, {type(ex)}") self.state_machine.current_state = self.states.FAULT @action() def start_acquisition_single(self): - self.stop_acquisition() # Just a shield - self._acquisition_thread = threading.Thread(target=self.measure, args=(1,)) + self.stop_acquisition() # Just a shield + self._acquisition_thread = threading.Thread(target=self.measure, args=(1,)) self._acquisition_thread.start() self.logger.info("data event will be pushed once acquisition is complete.") @@ -282,26 +297,34 @@ def test_echo(self, value): initial_state=states.DISCONNECTED, push_state_change_event=True, DISCONNECTED=[connect, serial_number], - ON=[start_acquisition, start_acquisition_single, disconnect, - integration_time, trigger_mode, background_correction, nonlinearity_correction], + ON=[ + start_acquisition, + start_acquisition_single, + disconnect, + integration_time, + trigger_mode, + background_correction, + nonlinearity_correction, + ], MEASURING=[stop_acquisition], - FAULT=[stop_acquisition, reset_fault] + FAULT=[stop_acquisition, reset_fault], ) logger_remote_access = True + def run_zmq_server(): - thing = OceanOpticsSpectrometer(id='test_spectrometer') + thing = OceanOpticsSpectrometer(id="test_spectrometer") thing.run_with_zmq_server() def run_http_server(): - thing = OceanOpticsSpectrometer(id='test_spectrometer') + thing = OceanOpticsSpectrometer(id="test_spectrometer") server = HTTPServer() server.add_things(thing) server.listen() -if __name__ == '__main__': +if __name__ == "__main__": run_zmq_server() # run_http_server() diff --git a/tests/things/starter.py b/tests/things/starter.py index 88c681c5..e0bd3b3b 100644 --- a/tests/things/starter.py +++ b/tests/things/starter.py @@ -102,9 +102,7 @@ def run_thing_with_zmq_server_forked( return T -def run_zmq_server( - server: AsyncZMQServer, owner, done_queue: multiprocessing.Queue -) -> None: +def run_zmq_server(server: AsyncZMQServer, owner, done_queue: multiprocessing.Queue) -> None: event_loop = get_current_async_loop() async def run(): diff --git a/tests/things/test_thing.py b/tests/things/test_thing.py index 98d4470e..6b797155 100644 --- a/tests/things/test_thing.py +++ b/tests/things/test_thing.py @@ -1,4 +1,6 @@ -import asyncio, threading, time, logging, unittest, os +import asyncio +import threading +import time import typing from pydantic import BaseModel, Field @@ -11,35 +13,21 @@ Integer, ClassSelector, ) -from hololinked.core.actions import ( - Action, - BoundAction, - BoundSyncAction, - BoundAsyncAction, -) +from hololinked.core.actions import Action, BoundAction from hololinked.param import ParameterizedFunction -from hololinked.core.dataklasses import ActionInfoValidator -from hololinked.utils import isclassmethod class TestThing(Thing): @action() def get_transports(self): transports = [] - if ( - self.rpc_server.req_rep_server is not None - and self.rpc_server.req_rep_server.socket_address.startswith("inproc://") + if self.rpc_server.req_rep_server is not None and self.rpc_server.req_rep_server.socket_address.startswith( + "inproc://" ): transports.append("INPROC") - if ( - self.rpc_server.ipc_server is not None - and self.rpc_server.ipc_server.socket_address.startswith("ipc://") - ): + if self.rpc_server.ipc_server is not None and self.rpc_server.ipc_server.socket_address.startswith("ipc://"): transports.append("IPC") - if ( - self.rpc_server.tcp_server is not None - and self.rpc_server.tcp_server.socket_address.startswith("tcp://") - ): + if self.rpc_server.tcp_server is not None and self.rpc_server.tcp_server.socket_address.startswith("tcp://"): transports.append("TCP") return transports @@ -70,9 +58,7 @@ class parameterized_action(ParameterizedFunction): doc="arg1 description", ) arg2 = String(default="hello", doc="arg2 description", regex="[a-z]+") - arg3 = ClassSelector( - class_=(int, float, str), default=5, doc="arg3 description" - ) + arg3 = ClassSelector(class_=(int, float, str), default=5, doc="arg3 description") def __call__(self, instance, arg1, arg2, arg3): return instance.id, arg1, arg2, arg3 @@ -86,9 +72,7 @@ class parameterized_action_without_call(ParameterizedFunction): doc="arg1 description", ) arg2 = String(default="hello", doc="arg2 description", regex="[a-z]+") - arg3 = ClassSelector( - class_=(int, float, str), default=5, doc="arg3 description" - ) + arg3 = ClassSelector(class_=(int, float, str), default=5, doc="arg3 description") class parameterized_action_async(ParameterizedFunction): arg1 = Number( @@ -99,9 +83,7 @@ class parameterized_action_async(ParameterizedFunction): doc="arg1 description", ) arg2 = String(default="hello", doc="arg2 description", regex="[a-z]+") - arg3 = ClassSelector( - class_=(int, float, str), default=5, doc="arg3 description" - ) + arg3 = ClassSelector(class_=(int, float, str), default=5, doc="arg3 description") async def __call__(self, instance, arg1, arg2, arg3): await asyncio.sleep(0.1) @@ -120,9 +102,7 @@ async def not_an_async_action(self, value): await asyncio.sleep(0.1) return value - def json_schema_validated_action( - self, val1: int, val2: str, val3: dict, val4: list - ): + def json_schema_validated_action(self, val1: int, val2: str, val3: dict, val4: list): return {"val1": val1, "val3": val3} def pydantic_validated_action( @@ -145,54 +125,63 @@ def sleep(self): # ----------- Properties -------------- base_property = Property(default=None, allow_None=True, doc="a base Property class") + number_prop = Number(doc="A fully editable number property", default=1) + string_prop = String( default="hello", regex="^[a-z]+", doc="A string property with a regex constraint to check value errors", ) + int_prop = Integer( default=5, step=2, bounds=(0, 100), doc="An integer property with step and bounds constraints to check RW", ) - selector_prop = Selector( - objects=["a", "b", "c", 1], default="a", doc="A selector property to check RW" - ) + + selector_prop = Selector(objects=["a", "b", "c", 1], default="a", doc="A selector property to check RW") + observable_list_prop = List( default=None, allow_None=True, observable=True, doc="An observable list property to check observable events on write operations", ) + observable_readonly_prop = Number( default=0, readonly=True, observable=True, doc="An observable readonly property to check observable events on read operations", ) + db_commit_number_prop = Number( default=0, db_commit=True, doc="A fully editable number property to check commits to db on write operations", ) + db_init_int_prop = Integer( default=1, db_init=True, doc="An integer property to check initialization from db", ) + db_persist_selector_prop = Selector( objects=["a", "b", "c", 1], default="a", db_persist=True, doc="A selector property to check persistence to db on write operations", ) + non_remote_number_prop = Number( default=5, remote=False, doc="A non remote number property to check non-availability on client", ) + sleeping_prop = Number( default=0, observable=True, @@ -262,13 +251,9 @@ def get_observable_readonly_prop(self): # ----------- Class properties -------------- - simple_class_prop = Number( - class_member=True, default=42, doc="simple class property with default value" - ) + simple_class_prop = Number(class_member=True, default=42, doc="simple class property with default value") - managed_class_prop = Number( - class_member=True, doc="(managed) class property with custom getter/setter" - ) + managed_class_prop = Number(class_member=True, doc="(managed) class property with custom getter/setter") @managed_class_prop.getter def get_managed_class_prop(cls): @@ -280,9 +265,7 @@ def set_managed_class_prop(cls, value): raise ValueError("Value must be non-negative") cls._managed_value = value - readonly_class_prop = String( - class_member=True, readonly=True, doc="read-only class property" - ) + readonly_class_prop = String(class_member=True, readonly=True, doc="read-only class property") @readonly_class_prop.getter def get_readonly_class_prop(cls): @@ -307,9 +290,7 @@ def del_deletable_class_prop(cls): if hasattr(cls, "_deletable_value"): del cls._deletable_value - not_a_class_prop = Number( - class_member=False, default=43, doc="test property with class_member=False" - ) + not_a_class_prop = Number(class_member=False, default=43, doc="test property with class_member=False") @not_a_class_prop.getter def get_not_a_class_prop(self): @@ -341,23 +322,15 @@ def print_props(self): test_event = Event(doc="test event with arbitrary payload") - total_number_of_events = Number( - default=100, bounds=(1, None), doc="Total number of events pushed" - ) + total_number_of_events = Number(default=100, bounds=(1, None), doc="Total number of events pushed") @action() - def push_events( - self, event_name: str = "test_event", total_number_of_events: int = 100 - ): + def push_events(self, event_name: str = "test_event", total_number_of_events: int = 100): if event_name not in self.events: raise ValueError(f"Event {event_name} is not a valid event") - threading.Thread( - target=self._push_worker, args=(event_name, total_number_of_events) - ).start() + threading.Thread(target=self._push_worker, args=(event_name, total_number_of_events)).start() - def _push_worker( - self, event_name: str = "test_event", total_number_of_events: int = 100 - ): + def _push_worker(self, event_name: str = "test_event", total_number_of_events: int = 100): for i in range(total_number_of_events): event_descriptor = self.events.descriptors[event_name] if event_descriptor == self.__class__.test_event: @@ -392,15 +365,11 @@ def _push_worker( test_binary_payload_event = Event(doc="test event with binary payload") - test_mixed_content_payload_event = Event( - doc="test event with mixed content payload" - ) + test_mixed_content_payload_event = Event(doc="test event with mixed content payload") test_event_with_json_schema = Event(doc="test event with schema validation") - test_event_with_pydantic_schema = Event( - doc="test event with pydantic schema validation" - ) + test_event_with_pydantic_schema = Event(doc="test event with pydantic schema validation") # --- Examples from existing device implementations @@ -444,13 +413,9 @@ def _push_worker( input_schema=analog_offset_input_schema, output_schema=analog_offset_output_schema, ) - def get_analogue_offset( - self, voltage_range: str, coupling: str - ) -> typing.Tuple[float, float]: + def get_analogue_offset(self, voltage_range: str, coupling: str) -> typing.Tuple[float, float]: """analogue offset for a voltage range and coupling""" - print( - f"get_analogue_offset called with voltage_range={voltage_range}, coupling={coupling}" - ) + print(f"get_analogue_offset called with voltage_range={voltage_range}, coupling={coupling}") return 0.0, 0.0 set_channel_schema = { @@ -536,9 +501,7 @@ def set_channel_pydantic( # ---- Gentec Optical Energy Meter - @action( - input_schema={"type": "string", "enum": ["QE25LP-S-MB", "QE12LP-S-MB-QED-D0"]} - ) + @action(input_schema={"type": "string", "enum": ["QE25LP-S-MB", "QE12LP-S-MB-QED-D0"]}) def set_sensor_model(self, value: str): """ Set the attached sensor to the meter under control. @@ -547,9 +510,7 @@ def set_sensor_model(self, value: str): print(f"set_sensor_model called with value={value}") @action() - def set_sensor_model_pydantic( - self, value: typing.Literal["QE25LP-S-MB", "QE12LP-S-MB-QED-D0"] - ): + def set_sensor_model_pydantic(self, value: typing.Literal["QE25LP-S-MB", "QE12LP-S-MB-QED-D0"]): """ Set the attached sensor to the meter under control. Sensor should be defined as a class and added to the AllowedSensors dict. @@ -582,17 +543,13 @@ def start_acquisition(self, max_count: typing.Annotated[int, Field(gt=0)]): # ----- Serial Utility @action() - def execute_instruction( - self, command: str, return_data_size: typing.Annotated[int, Field(ge=0)] = 0 - ) -> str: + def execute_instruction(self, command: str, return_data_size: typing.Annotated[int, Field(ge=0)] = 0) -> str: """ executes instruction given by the ASCII string parameter 'command'. If return data size is greater than 0, it reads the response and returns the response. Return Data Size - in bytes - 1 ASCII character = 1 Byte. """ - print( - f"execute_instruction called with command={command}, return_data_size={return_data_size}" - ) + print(f"execute_instruction called with command={command}, return_data_size={return_data_size}") return b"" @@ -605,9 +562,7 @@ def replace_methods_with_actions(thing_cls: typing.Type[TestThing]) -> None: if not isinstance(thing_cls.action_echo_with_classmethod, (Action, BoundAction)): # classmethod can be decorated with action - thing_cls.action_echo_with_classmethod = action()( - thing_cls.action_echo_with_classmethod - ) + thing_cls.action_echo_with_classmethod = action()(thing_cls.action_echo_with_classmethod) # BoundAction already, cannot call __set_name__ on it, at least at the time of writing exposed_actions.append("action_echo_with_classmethod") @@ -617,42 +572,28 @@ def replace_methods_with_actions(thing_cls: typing.Type[TestThing]) -> None: thing_cls.action_echo_async.__set_name__(thing_cls, "action_echo_async") exposed_actions.append("action_echo_async") - if not isinstance( - thing_cls.action_echo_async_with_classmethod, (Action, BoundAction) - ): + if not isinstance(thing_cls.action_echo_async_with_classmethod, (Action, BoundAction)): # async classmethods can be decorated with action - thing_cls.action_echo_async_with_classmethod = action()( - thing_cls.action_echo_async_with_classmethod - ) + thing_cls.action_echo_async_with_classmethod = action()(thing_cls.action_echo_async_with_classmethod) # BoundAction already, cannot call __set_name__ on it, at least at the time of writing exposed_actions.append("action_echo_async_with_classmethod") if not isinstance(thing_cls.parameterized_action, (Action, BoundAction)): # parameterized function can be decorated with action - thing_cls.parameterized_action = action(safe=True)( - thing_cls.parameterized_action - ) + thing_cls.parameterized_action = action(safe=True)(thing_cls.parameterized_action) thing_cls.parameterized_action.__set_name__(thing_cls, "parameterized_action") exposed_actions.append("parameterized_action") - if not isinstance( - thing_cls.parameterized_action_without_call, (Action, BoundAction) - ): + if not isinstance(thing_cls.parameterized_action_without_call, (Action, BoundAction)): thing_cls.parameterized_action_without_call = action(idempotent=True)( thing_cls.parameterized_action_without_call ) - thing_cls.parameterized_action_without_call.__set_name__( - thing_cls, "parameterized_action_without_call" - ) + thing_cls.parameterized_action_without_call.__set_name__(thing_cls, "parameterized_action_without_call") exposed_actions.append("parameterized_action_without_call") if not isinstance(thing_cls.parameterized_action_async, (Action, BoundAction)): - thing_cls.parameterized_action_async = action(synchronous=True)( - thing_cls.parameterized_action_async - ) - thing_cls.parameterized_action_async.__set_name__( - thing_cls, "parameterized_action_async" - ) + thing_cls.parameterized_action_async = action(synchronous=True)(thing_cls.parameterized_action_async) + thing_cls.parameterized_action_async.__set_name__(thing_cls, "parameterized_action_async") exposed_actions.append("parameterized_action_async") if not isinstance(thing_cls.json_schema_validated_action, (Action, BoundAction)): @@ -672,18 +613,12 @@ def replace_methods_with_actions(thing_cls: typing.Type[TestThing]) -> None: "properties": {"val1": {"type": "integer"}, "val3": {"type": "object"}}, }, )(thing_cls.json_schema_validated_action) - thing_cls.json_schema_validated_action.__set_name__( - thing_cls, "json_schema_validated_action" - ) + thing_cls.json_schema_validated_action.__set_name__(thing_cls, "json_schema_validated_action") exposed_actions.append("json_schema_validated_action") if not isinstance(thing_cls.pydantic_validated_action, (Action, BoundAction)): - thing_cls.pydantic_validated_action = action()( - thing_cls.pydantic_validated_action - ) - thing_cls.pydantic_validated_action.__set_name__( - thing_cls, "pydantic_validated_action" - ) + thing_cls.pydantic_validated_action = action()(thing_cls.pydantic_validated_action) + thing_cls.pydantic_validated_action.__set_name__(thing_cls, "pydantic_validated_action") exposed_actions.append("pydantic_validated_action") replace_methods_with_actions._exposed_actions = exposed_actions From e67d6b1657266cebf2de5799f51d3635f19723da Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 10:58:26 +0200 Subject: [PATCH 07/14] code style & general updates --- hololinked/schema_validators/json_schema.py | 4 +- tests/helper-scripts/design_scripty.ipynb | 41 ++++++++++++++----- .../{design_script.py => run_test_thing.py} | 2 + tests/things/test_thing.py | 41 ++++++++++++++++--- 4 files changed, 71 insertions(+), 17 deletions(-) rename tests/helper-scripts/{design_script.py => run_test_thing.py} (91%) diff --git a/hololinked/schema_validators/json_schema.py b/hololinked/schema_validators/json_schema.py index ee6ba58b..0a5c68ac 100644 --- a/hololinked/schema_validators/json_schema.py +++ b/hololinked/schema_validators/json_schema.py @@ -59,7 +59,7 @@ def register_type_replacement( self, type: typing.Any, json_schema_base_type: str, schema: typing.Optional[JSON] = None ) -> None: """ - Specify a python type to map to a specific JSON type. Schema only supported for array and objects. + Specify a python type to map to a specific JSON type. For example: - `JSONSchema.register_type_replacement(MyCustomObject, 'object', schema=MyCustomObject.schema())` @@ -87,7 +87,7 @@ def register_type_replacement( JSONSchema._schemas[type] = schema else: raise TypeError( - f"json schema replacement type must be one of allowed type - 'string', 'object', 'array', 'string', " + "json schema replacement type must be one of allowed type - 'string', 'object', 'array', 'string', " + f"'number', 'integer', 'boolean', 'null'. Given value {json_schema_base_type}" ) diff --git a/tests/helper-scripts/design_scripty.ipynb b/tests/helper-scripts/design_scripty.ipynb index ce696bff..c41086c1 100644 --- a/tests/helper-scripts/design_scripty.ipynb +++ b/tests/helper-scripts/design_scripty.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "c5ea7132", "metadata": {}, "outputs": [], @@ -2174,7 +2174,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 2, "id": "b67beb49", "metadata": {}, "outputs": [ @@ -2182,10 +2182,10 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO - 2025-10-04T09:04:47:988 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|sync and connected\n", - "INFO - 2025-10-04T09:04:47:990 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - sent Handshake to server 'example-test-server'\n", - "INFO - 2025-10-04T09:04:47:991 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - client 'example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|sync' handshook with server 'example-test-server'\n", - "INFO - 2025-10-04T09:04:47:997 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|async and connected\n" + "INFO - 2025-10-04T10:10:18:292 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|sync and connected\n", + "INFO - 2025-10-04T10:10:18:294 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T10:10:18:295 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - client 'example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|sync' handshook with server 'example-test-server'\n", + "INFO - 2025-10-04T10:10:18:300 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|async and connected\n" ] }, { @@ -2194,6 +2194,7 @@ "{'context': ['https://www.w3.org/2022/wot/td/v1.1'],\n", " 'id': 'http://127.0.0.1:9000/example-test',\n", " 'title': 'TestThing',\n", + " 'description': 'A test thing with various API options for properties, actions and events that were collected from examples from real world implementations, testing, features offered etc. Add your own use case here as needed.',\n", " 'properties': {'db_commit_number_prop': {'description': 'A fully editable number property to check commits to db on write operations',\n", " 'default': 0,\n", " 'type': 'number',\n", @@ -2300,6 +2301,16 @@ " 'op': 'writeproperty',\n", " 'htv:methodName': 'PUT',\n", " 'contentType': 'application/json'}]},\n", + " 'numpy_array_prop': {'description': 'A property with a numpy array as value',\n", + " 'oneOf': [{'type': 'array'}, {'type': 'null'}],\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/numpy-array-prop',\n", + " 'op': 'readproperty',\n", + " 'htv:methodName': 'GET',\n", + " 'contentType': 'application/msgpack'},\n", + " {'href': 'http://127.0.0.1:9000/example-test/numpy-array-prop',\n", + " 'op': 'writeproperty',\n", + " 'htv:methodName': 'PUT',\n", + " 'contentType': 'application/msgpack'}]},\n", " 'observable_list_prop': {'description': 'An observable list property to check observable events on write operations',\n", " 'oneOf': [{'type': 'array'}, {'type': 'null'}],\n", " 'observable': True,\n", @@ -2664,7 +2675,7 @@ " 'security': ['nosec']}" ] }, - "execution_count": 18, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" }, @@ -2672,8 +2683,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO - 2025-10-04T09:04:48:029 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - sent Handshake to server 'example-test-server'\n", - "INFO - 2025-10-04T09:04:48:030 - example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297 - client 'example-test-server|example-test|IPC|9d199fbf-f1db-41de-89e7-964650bb8297|async' handshook with server 'example-test-server'\n" + "INFO - 2025-10-04T10:10:18:358 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T10:10:18:360 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - client 'example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|async' handshook with server 'example-test-server'\n" ] } ], @@ -3061,7 +3072,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 3, "id": "0077604d", "metadata": {}, "outputs": [ @@ -3086,6 +3097,16 @@ "id": "7a9a4f24", "metadata": {}, "outputs": [], + "source": [ + "print(object_proxy.numpy_array_prop)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fba9e7c8", + "metadata": {}, + "outputs": [], "source": [] } ], diff --git a/tests/helper-scripts/design_script.py b/tests/helper-scripts/run_test_thing.py similarity index 91% rename from tests/helper-scripts/design_script.py rename to tests/helper-scripts/run_test_thing.py index 51625ad5..314592f4 100644 --- a/tests/helper-scripts/design_script.py +++ b/tests/helper-scripts/run_test_thing.py @@ -16,6 +16,8 @@ Serializers.register_for_object(TestThing.db_init_int_prop, Serializers.pickle) Serializers.register_for_object(TestThing.set_non_remote_number_prop, Serializers.msgpack) Serializers.register_for_object(TestThing.get_non_remote_number_prop, Serializers.msgpack) +Serializers.register_for_object(TestThing.numpy_array_prop, Serializers.msgpack) + # thing.run( # access_points=[ # ("ZMQ", "IPC"), diff --git a/tests/things/test_thing.py b/tests/things/test_thing.py index 6b797155..ebba3992 100644 --- a/tests/things/test_thing.py +++ b/tests/things/test_thing.py @@ -2,6 +2,7 @@ import threading import time import typing +import numpy as np from pydantic import BaseModel, Field from hololinked.core import Thing, action, Property, Event @@ -15,19 +16,27 @@ ) from hololinked.core.actions import Action, BoundAction from hololinked.param import ParameterizedFunction +from hololinked.schema_validators import JSONSchema class TestThing(Thing): + """ + A test thing with various API options for properties, actions and events that were collected from examples from + real world implementations, testing, features offered etc. + + Add your own use case here as needed. + """ + + # ----------- Actions -------------- + @action() def get_transports(self): transports = [] - if self.rpc_server.req_rep_server is not None and self.rpc_server.req_rep_server.socket_address.startswith( - "inproc://" - ): + if self.rpc_server.req_rep_server and self.rpc_server.req_rep_server.socket_address.startswith("inproc://"): transports.append("INPROC") - if self.rpc_server.ipc_server is not None and self.rpc_server.ipc_server.socket_address.startswith("ipc://"): + if self.rpc_server.ipc_server and self.rpc_server.ipc_server.socket_address.startswith("ipc://"): transports.append("IPC") - if self.rpc_server.tcp_server is not None and self.rpc_server.tcp_server.socket_address.startswith("tcp://"): + if self.rpc_server.tcp_server and self.rpc_server.tcp_server.socket_address.startswith("tcp://"): transports.append("TCP") return transports @@ -318,6 +327,28 @@ def print_props(self): print(f"db_persist_selctor_prop: {self.db_persist_selector_prop}") print(f"non_remote_number_prop: {self.non_remote_number_prop}") + # ----------- Pythonic objects as properties -------------- + + numpy_array_prop = ClassSelector( + default=None, + allow_None=True, + class_=(np.ndarray,), + doc="A property with a numpy array as value", + ) + + @numpy_array_prop.setter + def set_numpy_array_prop(self, value): + self._numpy_array_prop = value + + @numpy_array_prop.getter + def get_numpy_array_prop(self): + try: + return self._numpy_array_prop + except AttributeError: + return np.array([1, 2, 3]) + + JSONSchema.register_type_replacement(np.ndarray, "array") + # ----------- Events -------------- test_event = Event(doc="test event with arbitrary payload") From 4565a913d43ee8c3546c97eedddaa1313ab9f20c Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 11:55:59 +0200 Subject: [PATCH 08/14] rearrange operation flow in RPC broker --- hololinked/core/zmq/brokers.py | 2 +- hololinked/core/zmq/rpc_server.py | 128 ++++++++++++++--------------- hololinked/serializers/payloads.py | 9 +- 3 files changed, 73 insertions(+), 66 deletions(-) diff --git a/hololinked/core/zmq/brokers.py b/hololinked/core/zmq/brokers.py index c266a0dc..6890e6cf 100644 --- a/hololinked/core/zmq/brokers.py +++ b/hololinked/core/zmq/brokers.py @@ -2228,7 +2228,7 @@ def interrupt_message(self) -> EventMessage: return EventMessage.craft_from_arguments( event_id=f"{self.id}/interrupting-server", sender_id=self.id, - payload=SerializableData("INTERRUPT"), + payload=SerializableData("INTERRUPT", content_type="application/json"), ) def exit(self): diff --git a/hololinked/core/zmq/rpc_server.py b/hololinked/core/zmq/rpc_server.py index 1757eb32..e203e9d9 100644 --- a/hololinked/core/zmq/rpc_server.py +++ b/hololinked/core/zmq/rpc_server.py @@ -20,7 +20,7 @@ set_global_event_loop_policy, ) from ...config import global_config -from ...serializers import Serializers +from ...serializers import Serializers, BaseSerializer from .message import ( EMPTY_BYTE, ERROR, @@ -33,7 +33,7 @@ from ..thing import Thing from ..property import Property from ..properties import TypedDict -from ..actions import BoundAction, action as remote_method +from ..actions import BoundAction from ..logger import LogHistoryHandler @@ -377,67 +377,38 @@ async def run_thing_instance(self, instance: Thing, scheduler: typing.Optional[" return_value = await self.execute_operation(instance, objekt, operation, payload, preserialized_payload) # handle return value - if ( - isinstance(return_value, tuple) - and len(return_value) == 2 - and (isinstance(return_value[1], bytes) or isinstance(return_value[1], PreserializedData)) - ): - if fetch_execution_logs: - return_value[0] = { - "return_value": return_value[0], - "execution_logs": list_handler.log_list, - } - payload = SerializableData( - return_value[0], - Serializers.for_object(thing_id, instance.__class__.__name__, objekt), - ) - if isinstance(return_value[1], bytes): - preserialized_payload = PreserializedData(return_value[1]) - # elif isinstance(return_value, PreserializedData): - # if fetch_execution_logs: - # return_value = { - # "return_value" : return_value.value, - # "execution_logs" : list_handler.log_list - # } - # payload = SerializableData(return_value.value, content_type='application/json') - # preserialized_payload = return_value - - elif isinstance(return_value, bytes): - payload = SerializableData(None, content_type="application/json") - preserialized_payload = PreserializedData(return_value) - else: - # complete thing execution context - if fetch_execution_logs: - return_value = { - "return_value": return_value, - "execution_logs": list_handler.log_list, - } - payload = SerializableData( - return_value, - Serializers.for_object(thing_id, instance.__class__.__name__, objekt), - ) - preserialized_payload = PreserializedData(EMPTY_BYTE, content_type="text/plain") + serialization = Serializers.for_object(thing_id, instance.__class__.__name__, objekt) + payload, preserialized_payload = self.format_return_value(return_value, serializer=serialization) + + # complete thing execution context + if fetch_execution_logs: + payload.value = dict(return_value=payload.value, execution_logs=list_handler.log_list) + + # raise any payload errors now + payload.require_serialized() + # set reply scheduler.last_operation_reply = (payload, preserialized_payload, REPLY) + except BreakInnerLoop: # exit the loop and stop the thing instance.logger.info( - "Thing {} with instance name {} exiting event loop.".format( - instance.__class__.__name__, instance.id - ) + "Thing {} with id {} exiting event loop.".format(instance.__class__.__name__, instance.id) ) - return_value = None + + # send a reply with None return value + payload, preserialized_payload = self.format_return_value(None, Serializers.json) + + # complete thing execution context if fetch_execution_logs: - return_value = { - "return_value": None, - "execution_logs": list_handler.log_list, - } - scheduler.last_operation_reply = ( - SerializableData(return_value, content_type="application/json"), - PreserializedData(EMPTY_BYTE, content_type="text/plain"), - None, - ) - return + payload.value = dict(return_value=payload.value, execution_logs=list_handler.log_list) + + # set reply, let the message broker decide + scheduler.last_operation_reply = (payload, preserialized_payload, None) + + # quit the loop + break + except Exception as ex: # error occurred while executing the operation instance.logger.error( @@ -445,20 +416,25 @@ async def run_thing_instance(self, instance: Thing, scheduler: typing.Optional[" instance.__class__.__name__, instance.id, type(ex), ex ) ) - return_value = dict(exception=format_exception_as_json(ex)) - if fetch_execution_logs: - return_value["execution_logs"] = list_handler.log_list - scheduler.last_operation_reply = ( - SerializableData(return_value, content_type="application/json"), - PreserializedData(EMPTY_BYTE, content_type="text/plain"), - ERROR, + + # send a reply with error + payload, preserialized_payload = self.format_return_value( + dict(exception=format_exception_as_json(ex)), Serializers.json ) + + # complete thing execution context + if fetch_execution_logs: + payload.value["execution_logs"] = list_handler.log_list + + # set error reply + scheduler.last_operation_reply = (payload, preserialized_payload, ERROR) + finally: # cleanup if fetch_execution_logs: instance.logger.removeHandler(list_handler) instance.logger.debug( - "thing {} with instance name {} completed execution of operation {} on {}".format( + "thing {} with id {} completed execution of operation {} on {}".format( instance.__class__.__name__, instance.id, operation, objekt ) ) @@ -528,6 +504,30 @@ async def execute_operation( "Unimplemented execution path for Thing {} for operation {}".format(instance.id, operation) ) + def format_return_value( + self, + return_value: typing.Any, + serializer: BaseSerializer, + ) -> tuple[SerializableData, PreserializedData]: + if ( + isinstance(return_value, tuple) + and len(return_value) == 2 + and (isinstance(return_value[1], bytes) or isinstance(return_value[1], PreserializedData)) + ): + payload = SerializableData(return_value[0], serializer=serializer) + if isinstance(return_value[1], bytes): + preserialized_payload = PreserializedData(return_value[1]) + elif isinstance(return_value, bytes): + payload = SerializableData(None, content_type="application/json") + preserialized_payload = PreserializedData(return_value) + elif isinstance(return_value, PreserializedData): + payload = SerializableData(None, content_type="application/json") + preserialized_payload = return_value + else: + payload = SerializableData(return_value, serializer=serializer) + preserialized_payload = PreserializedData(EMPTY_BYTE, content_type="text/plain") + return payload, preserialized_payload + async def _process_timeouts( self, request_message: RequestMessage, diff --git a/hololinked/serializers/payloads.py b/hololinked/serializers/payloads.py index 428a3774..ca98851e 100644 --- a/hololinked/serializers/payloads.py +++ b/hololinked/serializers/payloads.py @@ -15,9 +15,12 @@ class SerializableData: value: typing.Any serializer: BaseSerializer | None = None content_type: str = "application/json" + _serialized: bytes | None = None def serialize(self): """serialize the value""" + if self._serialized is not None: + return self._serialized if isinstance(self.value, byte_types): return self.value if self.serializer is not None: @@ -38,6 +41,10 @@ def deserialize(self): return serializer.loads(self.value) raise ValueError(f"content type {self.content_type} not supported for deserialization") + def require_serialized(self) -> None: + """ensure the value is serialized""" + self._serialized = self.serialize() + @dataclass class PreserializedData: @@ -47,4 +54,4 @@ class PreserializedData: """ value: bytes - content_type: str = "unknown" + content_type: str = "application/octet-stream" From 18613620c672cf1a96d50c5346423c7b754147f7 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:26:29 +0200 Subject: [PATCH 09/14] support numpy in message pack --- hololinked/core/thing.py | 4 +- hololinked/serializers/serializers.py | 27 +- tests/helper-scripts/design_scripty.ipynb | 291 ++++------------------ 3 files changed, 78 insertions(+), 244 deletions(-) diff --git a/hololinked/core/thing.py b/hololinked/core/thing.py index 2b74813d..74ed55d3 100644 --- a/hololinked/core/thing.py +++ b/hololinked/core/thing.py @@ -349,9 +349,9 @@ def run( access_points = kwargs.get("access_points", None) # type: dict[str, dict | int | str | list[str]] servers = kwargs.get("servers", []) # type: typing.Optional[typing.List[BaseProtocolServer]] - if access_points is None and servers is None: + if access_points is None and len(servers) == 0: raise ValueError("At least one of access_points or servers must be provided.") - if access_points is not None and servers is not None: + if access_points is not None and len(servers) > 0: raise ValueError("Only one of access_points or servers can be provided.") if access_points is not None: diff --git a/hololinked/serializers/serializers.py b/hololinked/serializers/serializers.py index c4d419f4..31493b1e 100644 --- a/hololinked/serializers/serializers.py +++ b/hololinked/serializers/serializers.py @@ -27,6 +27,7 @@ import inspect import array import datetime +import io import uuid import decimal import typing @@ -221,10 +222,32 @@ def __init__(self) -> None: self.type = msgpack def dumps(self, value) -> bytes: - return msgpack.encode(value) + return msgpack.encode(value, enc_hook=self.default_encode) def loads(self, value) -> typing.Any: - return msgpack.decode(self.convert_to_bytes(value)) + value = msgpack.decode(self.convert_to_bytes(value)) + # TODO decoder hook not called, not sure why + if isinstance(value, (memoryview, bytearray, bytes)): + return self.default_decode(value) + + @classmethod + def default_encode(cls, obj) -> typing.Any: + if "numpy" in globals() and isinstance(obj, numpy.ndarray): + buf = io.BytesIO() + # .npy stores dtype, shape, order, endianness + numpy.save(buf, obj, allow_pickle=False) + return buf.getvalue() + raise TypeError("Given type cannot be converted to MessagePack : {}".format(type(obj))) + + @classmethod + def default_decode(cls, obj) -> typing.Any: + # If numpy is available and obj is a memoryview, convert back to numpy array + if "numpy" in globals(): + try: + return numpy.load(io.BytesIO(obj), allow_pickle=False) + except Exception: + pass + return obj @property def content_type(self) -> str: diff --git a/tests/helper-scripts/design_scripty.ipynb b/tests/helper-scripts/design_scripty.ipynb index c41086c1..fb7705e3 100644 --- a/tests/helper-scripts/design_scripty.ipynb +++ b/tests/helper-scripts/design_scripty.ipynb @@ -31,228 +31,7 @@ "execution_count": null, "id": "0ded1e1a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO - 2025-09-21T09:28:11:539 - example-test - initialialised Thing class TestThing with id example-test\n", - "INFO - 2025-09-21T09:28:11:543 - example-test - created socket type: ROUTER with address: inproc://example-test & identity: example-test and bound\n", - "INFO - 2025-09-21T09:28:11:547 - example-test - created socket type: PUB with address: inproc://example-test/event-publisher & identity: example-test/event-publisher and bound\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO - 2025-09-21T09:28:11:550 - example-test - created socket type: ROUTER with address: tcp://*:5558 & identity: example-test and bound\n", - "INFO - 2025-09-21T09:28:11:562 - example-test - created socket type: PUB with address: tcp://0.0.0.0:60000 & identity: example-test/event-publisher and bound\n", - "INFO - 2025-09-21T09:28:11:565 - example-test - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test.ipc & identity: example-test and bound\n", - "INFO - 2025-09-21T09:28:11:568 - example-test - created socket type: PUB with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test\\event-publisher.ipc & identity: example-test/event-publisher and bound\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO - 2025-09-21T09:28:11:570 - example-test - created socket type: SUB with address: inproc://example-test/event-publisher & identity: example-test/event-proxy and connected\n", - "INFO - 2025-09-21T09:28:11:627 - HTTPServer|0.0.0.0:8080 - created socket type: ROUTER with address: inproc://example-test & identity: 0.0.0.0:8080 and connected\n", - "INFO - 2025-09-21T09:28:11:630 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:12:131 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:12:132 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:12:646 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:12:647 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:13:161 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:13:162 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:13:664 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:13:664 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:14:176 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:14:177 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:14:681 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:14:682 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:15:193 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:15:194 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:15:697 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:15:698 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:16:211 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:16:212 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:16:717 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:16:717 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:17:227 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:17:228 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:17:734 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:17:734 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:18:245 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:18:246 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:18:751 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:18:752 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:19:260 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:19:261 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:19:767 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:19:768 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:20:269 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:20:270 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:20:779 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:20:780 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:21:285 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:21:285 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:21:798 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:21:799 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:22:304 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:22:304 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:22:817 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:22:818 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:23:322 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:23:322 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:23:835 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:23:836 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:24:339 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:24:340 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:24:851 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:24:852 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:25:356 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:25:357 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:25:867 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:25:867 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:26:373 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:26:374 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:26:885 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:26:886 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:27:391 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:27:392 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:27:905 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:27:906 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:28:409 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:28:410 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:28:920 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:28:920 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:29:427 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:29:428 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:29:940 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:29:940 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:30:444 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:30:445 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:30:956 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:30:956 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:31:461 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:31:462 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:31:974 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:31:975 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:32:478 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:32:479 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:32:991 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:32:992 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:33:495 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:33:496 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:34:009 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:34:010 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:34:514 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:34:515 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:35:025 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:35:025 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:35:531 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:35:532 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:36:048 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:36:049 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:36:564 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:36:565 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:37:066 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:37:067 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:37:580 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:37:581 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:38:083 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:38:084 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:38:596 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:38:598 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:39:101 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:39:102 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:39:612 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:39:614 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:40:119 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:40:120 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:40:631 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:40:632 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "INFO - 2025-09-21T09:28:41:135 - HTTPServer|0.0.0.0:8080 - got no response for handshake\n", - "INFO - 2025-09-21T09:28:41:136 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", - "ERROR - 2025-09-21T09:28:41:635 - HTTPServer|0.0.0.0:8080 - could not connect to example-test using on server example-test with access_point INPROC. error: \n", - "INFO - 2025-09-21T09:28:41:636 - example-test - starting RPC server example-test\n", - "INFO - 2025-09-21T09:28:41:638 - example-test - starting thing executor loop in thread 21816 for ['example-test']\n", - "INFO - 2025-09-21T09:28:41:642 - example-test - starting to run operations on thing example-test of class TestThing\n", - "INFO - 2025-09-21T09:28:41:642 - example-test - starting external message listener thread\n", - "INFO - 2025-09-21T09:28:41:647 - HTTPServer|0.0.0.0:8080 - started webserver at 0.0.0.0:8080, ready to receive requests.\n", - "INFO - 2025-09-21T09:28:41:650 - HTTPServer|0.0.0.0:8080 - client polling started for sockets for []\n", - "INFO - 2025-09-21T09:28:41:654 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:656 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:657 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:658 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:659 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:661 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:662 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:663 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:664 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:665 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:666 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:667 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:668 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:670 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:671 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:672 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:674 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:675 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:676 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:677 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:678 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:680 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:680 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:681 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:682 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:683 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:684 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:685 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:686 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:686 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:687 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:688 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:690 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:690 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:691 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:692 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:692 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:693 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:693 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:694 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:695 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:695 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:696 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:696 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:697 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:697 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:698 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:698 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:700 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:701 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:702 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:703 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:704 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:704 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:705 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:706 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:707 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:708 - example-test - sent handshake to client '0.0.0.0:8080'\n", - "INFO - 2025-09-21T09:28:41:709 - example-test - sent handshake to client '0.0.0.0:8080'\n" - ] - } - ], + "outputs": [], "source": [ "thing = TestThing(id='example-test')\n", "thing.run(\n", @@ -2182,10 +1961,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO - 2025-10-04T10:10:18:292 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|sync and connected\n", - "INFO - 2025-10-04T10:10:18:294 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - sent Handshake to server 'example-test-server'\n", - "INFO - 2025-10-04T10:10:18:295 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - client 'example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|sync' handshook with server 'example-test-server'\n", - "INFO - 2025-10-04T10:10:18:300 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|async and connected\n" + "INFO - 2025-10-04T12:13:00:793 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|sync and connected\n", + "INFO - 2025-10-04T12:13:00:793 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T12:13:01:301 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - got no response for handshake\n", + "INFO - 2025-10-04T12:13:01:302 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T12:13:01:303 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - client 'example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|sync' handshook with server 'example-test-server'\n", + "INFO - 2025-10-04T12:13:01:309 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|async and connected\n" ] }, { @@ -2683,8 +2464,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO - 2025-10-04T10:10:18:358 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - sent Handshake to server 'example-test-server'\n", - "INFO - 2025-10-04T10:10:18:360 - example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974 - client 'example-test-server|example-test|IPC|965931b9-9e0f-43ba-9b61-31e7c642f974|async' handshook with server 'example-test-server'\n" + "INFO - 2025-10-04T12:13:01:346 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T12:13:01:347 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - client 'example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|async' handshook with server 'example-test-server'\n" ] } ], @@ -3007,29 +2788,29 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 3, "id": "41595859", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "15" + "25" ] }, - "execution_count": 15, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "object_proxy.set_non_remote_number_prop(15)\n", + "object_proxy.set_non_remote_number_prop(25)\n", "object_proxy.get_non_remote_number_prop()" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 4, "id": "113441d1", "metadata": {}, "outputs": [ @@ -3039,7 +2820,7 @@ "['INPROC', 'IPC']" ] }, - "execution_count": 16, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -3050,7 +2831,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 5, "id": "ea4ed954", "metadata": {}, "outputs": [ @@ -3060,7 +2841,7 @@ "b'foobar'" ] }, - "execution_count": 20, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -3072,7 +2853,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "id": "0077604d", "metadata": {}, "outputs": [ @@ -3093,19 +2874,49 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "7a9a4f24", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00'\n" + ] + } + ], "source": [ "print(object_proxy.numpy_array_prop)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "fba9e7c8", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([4.9e-324, 9.9e-324, 1.5e-323])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "np.frombuffer(b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92c360e6", + "metadata": {}, "outputs": [], "source": [] } From dd1e5b0e7f94701e04dbd4e474313feb4ea8faa7 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 16:15:06 +0200 Subject: [PATCH 10/14] some more bug fix --- .../client/http/consumed_interactions.py | 3 ++- hololinked/core/property.py | 9 +++++-- hololinked/core/zmq/rpc_server.py | 26 +++++++++---------- hololinked/serializers/serializers.py | 1 + hololinked/utils.py | 7 +++-- tests/helper-scripts/run_test_thing.py | 1 + tests/things/test_thing.py | 18 +++++++++++-- 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/hololinked/client/http/consumed_interactions.py b/hololinked/client/http/consumed_interactions.py index 3e47c403..7933d8d7 100644 --- a/hololinked/client/http/consumed_interactions.py +++ b/hololinked/client/http/consumed_interactions.py @@ -49,7 +49,8 @@ def get_body_from_response( body = response.content if not body: return - serializer = Serializers.content_types.get(form.contentType or "application/json") + givenContentType = response.headers.get("Content-Type", None) + serializer = Serializers.content_types.get(givenContentType or form.contentType or "application/json") if serializer is None: raise ValueError(f"Unsupported content type: {form.contentType}") body = serializer.loads(body) diff --git a/hololinked/core/property.py b/hololinked/core/property.py index c1dbe129..fc906b15 100644 --- a/hololinked/core/property.py +++ b/hololinked/core/property.py @@ -329,7 +329,7 @@ def to_affordance(self, owner_inst=None): try: - from pydantic import BaseModel, RootModel, create_model + from pydantic import BaseModel, RootModel, create_model, ConfigDict def wrap_plain_types_in_rootmodel(model: type) -> type[BaseModel] | type[RootModel]: """ @@ -344,7 +344,12 @@ def wrap_plain_types_in_rootmodel(model: type) -> type[BaseModel] | type[RootMod return if issubklass(model, BaseModel): return model - return create_model(f"{model!r}", root=(model, ...), __base__=RootModel) + return create_model( + f"{model!r}", + root=(model, ...), + __base__=RootModel, + __config__=ConfigDict(arbitrary_types_allowed=True), + ) # type: ignore[call-overload] except ImportError: def wrap_plain_types_in_rootmodel(model: type) -> type: diff --git a/hololinked/core/zmq/rpc_server.py b/hololinked/core/zmq/rpc_server.py index e203e9d9..1759c769 100644 --- a/hololinked/core/zmq/rpc_server.py +++ b/hololinked/core/zmq/rpc_server.py @@ -377,18 +377,18 @@ async def run_thing_instance(self, instance: Thing, scheduler: typing.Optional[" return_value = await self.execute_operation(instance, objekt, operation, payload, preserialized_payload) # handle return value - serialization = Serializers.for_object(thing_id, instance.__class__.__name__, objekt) - payload, preserialized_payload = self.format_return_value(return_value, serializer=serialization) + serializer = Serializers.for_object(thing_id, instance.__class__.__name__, objekt) + rpayload, rpreserialized_payload = self.format_return_value(return_value, serializer=serializer) # complete thing execution context if fetch_execution_logs: - payload.value = dict(return_value=payload.value, execution_logs=list_handler.log_list) + rpayload.value = dict(return_value=rpayload.value, execution_logs=list_handler.log_list) # raise any payload errors now - payload.require_serialized() + rpayload.require_serialized() # set reply - scheduler.last_operation_reply = (payload, preserialized_payload, REPLY) + scheduler.last_operation_reply = (rpayload, rpreserialized_payload, REPLY) except BreakInnerLoop: # exit the loop and stop the thing @@ -397,14 +397,14 @@ async def run_thing_instance(self, instance: Thing, scheduler: typing.Optional[" ) # send a reply with None return value - payload, preserialized_payload = self.format_return_value(None, Serializers.json) + rpayload, rpreserialized_payload = self.format_return_value(None, Serializers.json) # complete thing execution context if fetch_execution_logs: - payload.value = dict(return_value=payload.value, execution_logs=list_handler.log_list) + rpayload.value = dict(return_value=rpayload.value, execution_logs=list_handler.log_list) # set reply, let the message broker decide - scheduler.last_operation_reply = (payload, preserialized_payload, None) + scheduler.last_operation_reply = (rpayload, rpreserialized_payload, None) # quit the loop break @@ -418,16 +418,16 @@ async def run_thing_instance(self, instance: Thing, scheduler: typing.Optional[" ) # send a reply with error - payload, preserialized_payload = self.format_return_value( + rpayload, rpreserialized_payload = self.format_return_value( dict(exception=format_exception_as_json(ex)), Serializers.json ) # complete thing execution context if fetch_execution_logs: - payload.value["execution_logs"] = list_handler.log_list + rpayload.value["execution_logs"] = list_handler.log_list # set error reply - scheduler.last_operation_reply = (payload, preserialized_payload, ERROR) + scheduler.last_operation_reply = (rpayload, rpreserialized_payload, ERROR) finally: # cleanup @@ -514,7 +514,7 @@ def format_return_value( and len(return_value) == 2 and (isinstance(return_value[1], bytes) or isinstance(return_value[1], PreserializedData)) ): - payload = SerializableData(return_value[0], serializer=serializer) + payload = SerializableData(return_value[0], serializer=serializer, content_type=serializer.content_type) if isinstance(return_value[1], bytes): preserialized_payload = PreserializedData(return_value[1]) elif isinstance(return_value, bytes): @@ -524,7 +524,7 @@ def format_return_value( payload = SerializableData(None, content_type="application/json") preserialized_payload = return_value else: - payload = SerializableData(return_value, serializer=serializer) + payload = SerializableData(return_value, serializer=serializer, content_type=serializer.content_type) preserialized_payload = PreserializedData(EMPTY_BYTE, content_type="text/plain") return payload, preserialized_payload diff --git a/hololinked/serializers/serializers.py b/hololinked/serializers/serializers.py index 31493b1e..61dcd296 100644 --- a/hololinked/serializers/serializers.py +++ b/hololinked/serializers/serializers.py @@ -229,6 +229,7 @@ def loads(self, value) -> typing.Any: # TODO decoder hook not called, not sure why if isinstance(value, (memoryview, bytearray, bytes)): return self.default_decode(value) + return value @classmethod def default_encode(cls, obj) -> typing.Any: diff --git a/hololinked/utils.py b/hololinked/utils.py index 3dde9f04..b0efc349 100644 --- a/hololinked/utils.py +++ b/hololinked/utils.py @@ -446,7 +446,7 @@ def get_input_model_from_signature( model = create_model( # type: ignore[call-overload] f"{func.__name__}_input", **fields, - __config__=ConfigDict(extra="forbid", strict=True), + __config__=ConfigDict(extra="forbid", strict=True, arbitrary_types_allowed=True), ) return model @@ -598,7 +598,10 @@ def get_all_sub_things_recusively(thing) -> typing.List: def forkable(func): - """Decorator to make a function forkable. This is useful for functions that need to be run in a separate thread.""" + """ + Decorator to make a function forkable. + This is useful for functions that need to be run in a separate thread. + """ @wraps(func) def wrapper(*args, **kwargs): diff --git a/tests/helper-scripts/run_test_thing.py b/tests/helper-scripts/run_test_thing.py index 314592f4..5fa58294 100644 --- a/tests/helper-scripts/run_test_thing.py +++ b/tests/helper-scripts/run_test_thing.py @@ -17,6 +17,7 @@ Serializers.register_for_object(TestThing.set_non_remote_number_prop, Serializers.msgpack) Serializers.register_for_object(TestThing.get_non_remote_number_prop, Serializers.msgpack) Serializers.register_for_object(TestThing.numpy_array_prop, Serializers.msgpack) +Serializers.register_for_object(TestThing.numpy_action, Serializers.msgpack) # thing.run( # access_points=[ diff --git a/tests/things/test_thing.py b/tests/things/test_thing.py index ebba3992..c853c1bb 100644 --- a/tests/things/test_thing.py +++ b/tests/things/test_thing.py @@ -3,7 +3,7 @@ import time import typing import numpy as np -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, WithJsonSchema from hololinked.core import Thing, action, Property, Event from hololinked.core.properties import ( @@ -24,7 +24,7 @@ class TestThing(Thing): A test thing with various API options for properties, actions and events that were collected from examples from real world implementations, testing, features offered etc. - Add your own use case here as needed. + Add your own use case/snippets used in tests here as needed. """ # ----------- Actions -------------- @@ -349,6 +349,20 @@ def get_numpy_array_prop(self): JSONSchema.register_type_replacement(np.ndarray, "array") + NDArray = typing.Annotated[ + np.ndarray, + WithJsonSchema( + { + "type": "array", + "items": {"type": "number"}, + } + ), + ] + + @action() + def numpy_action(self, array: NDArray) -> NDArray: + return array * 2 + # ----------- Events -------------- test_event = Event(doc="test event with arbitrary payload") From b0886069acf7960a0bbb6f1646471f09a07083f7 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:02:49 +0200 Subject: [PATCH 11/14] finalize msgpack numpy with pydantic --- hololinked/core/zmq/rpc_server.py | 10 +- hololinked/serializers/serializers.py | 24 ++-- tests/helper-scripts/design_scripty.ipynb | 159 +++++++++++++++------- 3 files changed, 127 insertions(+), 66 deletions(-) diff --git a/hololinked/core/zmq/rpc_server.py b/hololinked/core/zmq/rpc_server.py index 1759c769..2fd4d36e 100644 --- a/hololinked/core/zmq/rpc_server.py +++ b/hololinked/core/zmq/rpc_server.py @@ -477,6 +477,12 @@ async def execute_operation( elif operation == Operations.deleteproperty: prop = instance.properties[objekt] # type: Property del prop # raises NotImplementedError when deletion is not implemented which is mostly the case + elif operation == Operations.invokeaction and objekt == "get_thing_description": + # special case + if payload is None: + payload = dict() + args = payload.pop("__args__", tuple()) + return self.get_thing_description(instance, *args, **payload) elif operation == Operations.invokeaction: if payload is None: payload = dict() @@ -484,10 +490,6 @@ async def execute_operation( # payload then become kwargs if preserialized_payload != EMPTY_BYTE: args = (preserialized_payload,) + args - # special case - if objekt == "get_thing_description": - return self.get_thing_description(instance, *args, **payload) - # normal Thing action action = instance.actions[objekt] # type: BoundAction if action.execution_info.iscoroutine: # the actual scheduling as a purely async task is done by the scheduler, not here, diff --git a/hololinked/serializers/serializers.py b/hololinked/serializers/serializers.py index 61dcd296..0c95c89c 100644 --- a/hololinked/serializers/serializers.py +++ b/hololinked/serializers/serializers.py @@ -221,33 +221,29 @@ def __init__(self) -> None: super().__init__() self.type = msgpack + codes = dict(NDARRAY_EXT=1) + def dumps(self, value) -> bytes: return msgpack.encode(value, enc_hook=self.default_encode) def loads(self, value) -> typing.Any: - value = msgpack.decode(self.convert_to_bytes(value)) - # TODO decoder hook not called, not sure why - if isinstance(value, (memoryview, bytearray, bytes)): - return self.default_decode(value) - return value + return msgpack.decode(self.convert_to_bytes(value), ext_hook=self.ext_decode) @classmethod def default_encode(cls, obj) -> typing.Any: if "numpy" in globals() and isinstance(obj, numpy.ndarray): buf = io.BytesIO() - # .npy stores dtype, shape, order, endianness - numpy.save(buf, obj, allow_pickle=False) - return buf.getvalue() + numpy.save(buf, obj, allow_pickle=False) # use .npy. which stores dtype, shape, order, endianness + return msgpack.Ext(MsgpackSerializer.codes["NDARRAY_EXT"], buf.getvalue()) raise TypeError("Given type cannot be converted to MessagePack : {}".format(type(obj))) @classmethod - def default_decode(cls, obj) -> typing.Any: - # If numpy is available and obj is a memoryview, convert back to numpy array - if "numpy" in globals(): - try: + def ext_decode(cls, code: int, obj: memoryview) -> typing.Any: + if code == MsgpackSerializer.codes["NDARRAY_EXT"]: + if "numpy" in globals(): return numpy.load(io.BytesIO(obj), allow_pickle=False) - except Exception: - pass + else: + raise ValueError("numpy is required to decode numpy array from MessagePack") return obj @property diff --git a/tests/helper-scripts/design_scripty.ipynb b/tests/helper-scripts/design_scripty.ipynb index fb7705e3..486ab129 100644 --- a/tests/helper-scripts/design_scripty.ipynb +++ b/tests/helper-scripts/design_scripty.ipynb @@ -31,7 +31,56 @@ "execution_count": null, "id": "0ded1e1a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO - 2025-10-04T16:54:24:934 - example-test - initialialised Thing class TestThing with id example-test\n", + "INFO - 2025-10-04T16:54:24:938 - example-test - created socket type: ROUTER with address: inproc://example-test & identity: example-test and bound\n", + "INFO - 2025-10-04T16:54:24:942 - example-test - created socket type: PUB with address: inproc://example-test/event-publisher & identity: example-test/event-publisher and bound\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO - 2025-10-04T16:54:24:944 - example-test - created socket type: ROUTER with address: tcp://*:5558 & identity: example-test and bound\n", + "INFO - 2025-10-04T16:54:24:946 - example-test - created socket type: PUB with address: tcp://*:5559 & identity: example-test/event-publisher and bound\n", + "INFO - 2025-10-04T16:54:24:950 - example-test - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test.ipc & identity: example-test and bound\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO - 2025-10-04T16:54:24:955 - example-test - created socket type: PUB with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test\\event-publisher.ipc & identity: example-test/event-publisher and bound\n", + "INFO - 2025-10-04T16:54:24:957 - example-test - created socket type: SUB with address: inproc://example-test/event-publisher & identity: example-test/event-proxy and connected\n", + "INFO - 2025-10-04T16:54:24:960 - example-test - starting RPC server example-test\n", + "INFO - 2025-10-04T16:54:24:962 - example-test - starting thing executor loop in thread 5956 for ['example-test']\n", + "INFO - 2025-10-04T16:54:24:965 - example-test - starting to run operations on thing example-test of class TestThing\n", + "INFO - 2025-10-04T16:54:24:965 - example-test - starting external message listener thread\n", + "INFO - 2025-10-04T16:54:25:132 - HTTPServer|0.0.0.0:8080 - created socket type: ROUTER with address: inproc://example-test & identity: 0.0.0.0:8080 and connected\n", + "INFO - 2025-10-04T16:54:25:136 - HTTPServer|0.0.0.0:8080 - sent Handshake to server 'example-test'\n", + "INFO - 2025-10-04T16:54:25:138 - example-test - sent handshake to client '0.0.0.0:8080'\n", + "INFO - 2025-10-04T16:54:25:138 - HTTPServer|0.0.0.0:8080 - client '0.0.0.0:8080' handshook with server 'example-test'\n", + "payload before deserialize SerializableData(value=b'{\"ignore_errors\":true,\"protocol\":\"INPROC\"}', serializer=None, content_type='application/json', _serialized=None)\n", + "ERROR - 2025-10-04T16:54:25:143 - example-test - Error while generating schema for base_property - WoT schema generator for this descriptor/property is not implemented. name base_property & type \n", + "INFO - 2025-10-04T16:54:25:160 - HTTPServer|0.0.0.0:8080 - started webserver at 0.0.0.0:8080, ready to receive requests.\n", + "INFO - 2025-10-04T16:54:25:160 - HTTPServer|0.0.0.0:8080 - client polling started for sockets for ['0.0.0.0:8080']\n" + ] + } + ], "source": [ "thing = TestThing(id='example-test')\n", "thing.run(\n", @@ -1951,6 +2000,32 @@ "TestThing.data_point_event.to_affordance().json()" ] }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e51e2fb7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'input': {'properties': {'array': {'items': {'type': 'number'},\n", + " 'type': 'array'}},\n", + " 'required': ['array'],\n", + " 'type': 'object'},\n", + " 'output': {'items': {'type': 'number'}, 'type': 'array'},\n", + " 'synchronous': True}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "TestThing.numpy_action.to_affordance().json()" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -1961,12 +2036,10 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO - 2025-10-04T12:13:00:793 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|sync and connected\n", - "INFO - 2025-10-04T12:13:00:793 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - sent Handshake to server 'example-test-server'\n", - "INFO - 2025-10-04T12:13:01:301 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - got no response for handshake\n", - "INFO - 2025-10-04T12:13:01:302 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - sent Handshake to server 'example-test-server'\n", - "INFO - 2025-10-04T12:13:01:303 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - client 'example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|sync' handshook with server 'example-test-server'\n", - "INFO - 2025-10-04T12:13:01:309 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|async and connected\n" + "INFO - 2025-10-04T17:01:27:554 - example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b|sync and connected\n", + "INFO - 2025-10-04T17:01:27:555 - example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T17:01:27:556 - example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b - client 'example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b|sync' handshook with server 'example-test-server'\n", + "INFO - 2025-10-04T17:01:27:560 - example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b - created socket type: ROUTER with address: ipc://C:\\Users\\vvign\\AppData\\Local\\Temp\\hololinked\\example-test-server.ipc & identity: example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b|async and connected\n" ] }, { @@ -1975,7 +2048,7 @@ "{'context': ['https://www.w3.org/2022/wot/td/v1.1'],\n", " 'id': 'http://127.0.0.1:9000/example-test',\n", " 'title': 'TestThing',\n", - " 'description': 'A test thing with various API options for properties, actions and events that were collected from examples from real world implementations, testing, features offered etc. Add your own use case here as needed.',\n", + " 'description': 'A test thing with various API options for properties, actions and events that were collected from examples from real world implementations, testing, features offered etc. Add your own use case/snippets used in tests here as needed.',\n", " 'properties': {'db_commit_number_prop': {'description': 'A fully editable number property to check commits to db on write operations',\n", " 'default': 0,\n", " 'type': 'number',\n", @@ -2296,6 +2369,16 @@ " 'op': 'invokeaction',\n", " 'htv:methodName': 'POST',\n", " 'contentType': 'application/json'}]},\n", + " 'numpy_action': {'input': {'properties': {'array': {'items': {'type': 'number'},\n", + " 'type': 'array'}},\n", + " 'required': ['array'],\n", + " 'type': 'object'},\n", + " 'output': {'items': {'type': 'number'}, 'type': 'array'},\n", + " 'synchronous': True,\n", + " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/numpy-action',\n", + " 'op': 'invokeaction',\n", + " 'htv:methodName': 'POST',\n", + " 'contentType': 'application/msgpack'}]},\n", " 'ping': {'description': 'ping to see if it is alive. Successful when action succeeds with no return value and no timeout or exception raised on the client side.',\n", " 'synchronous': True,\n", " 'forms': [{'href': 'http://127.0.0.1:9000/example-test/ping',\n", @@ -2464,8 +2547,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO - 2025-10-04T12:13:01:346 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - sent Handshake to server 'example-test-server'\n", - "INFO - 2025-10-04T12:13:01:347 - example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037 - client 'example-test-server|example-test|IPC|88e17723-9e0d-40af-9749-b67f64088037|async' handshook with server 'example-test-server'\n" + "INFO - 2025-10-04T17:01:27:612 - example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b - sent Handshake to server 'example-test-server'\n", + "INFO - 2025-10-04T17:01:27:614 - example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b - client 'example-test-server|example-test|IPC|d0adc808-b7ed-4a9e-a97f-8bb3f2c04a0b|async' handshook with server 'example-test-server'\n" ] } ], @@ -2744,7 +2827,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "3285efde", "metadata": {}, "outputs": [ @@ -2767,7 +2850,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 4, "id": "169fd06e", "metadata": {}, "outputs": [ @@ -2777,7 +2860,7 @@ "1" ] }, - "execution_count": 12, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -2788,29 +2871,29 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "id": "41595859", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "25" + "20" ] }, - "execution_count": 3, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "object_proxy.set_non_remote_number_prop(25)\n", - "object_proxy.get_non_remote_number_prop()" + "object_proxy_zmq.set_non_remote_number_prop(20)\n", + "object_proxy_zmq.get_non_remote_number_prop()" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "113441d1", "metadata": {}, "outputs": [ @@ -2820,7 +2903,7 @@ "['INPROC', 'IPC']" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -2831,7 +2914,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "id": "ea4ed954", "metadata": {}, "outputs": [ @@ -2841,7 +2924,7 @@ "b'foobar'" ] }, - "execution_count": 5, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -2853,7 +2936,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "id": "0077604d", "metadata": {}, "outputs": [ @@ -2874,7 +2957,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 9, "id": "7a9a4f24", "metadata": {}, "outputs": [ @@ -2882,34 +2965,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00'\n" + "[2 4 6]\n" ] } ], "source": [ - "print(object_proxy.numpy_array_prop)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "fba9e7c8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([4.9e-324, 9.9e-324, 1.5e-323])" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import numpy as np\n", - "np.frombuffer(b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00')" + "# print(object_proxy.numpy_array_prop)\n", + "# print(type(object_proxy.numpy_array_prop))\n", + "print(object_proxy.numpy_action(object_proxy.numpy_array_prop))" ] }, { From 2a29268911f65ffb63274470b5df179db2974b81 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:07:27 +0200 Subject: [PATCH 12/14] commit ipynb --- tests/helper-scripts/{design_scripty.ipynb => client.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/helper-scripts/{design_scripty.ipynb => client.ipynb} (100%) diff --git a/tests/helper-scripts/design_scripty.ipynb b/tests/helper-scripts/client.ipynb similarity index 100% rename from tests/helper-scripts/design_scripty.ipynb rename to tests/helper-scripts/client.ipynb From fdb17cac94ce523189ba522f62cd65db174bc79e Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:10:14 +0200 Subject: [PATCH 13/14] add a gitattributes --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..344b6c59 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.ipynb export-ignore +*.ipynb linguist-vendored \ No newline at end of file From d370026a10232cfe10a385d338a7baf5232ba05f Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Vaidyanathan <62492557+VigneshVSV@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:46:06 +0200 Subject: [PATCH 14/14] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f26d7f..0a2d8abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [v0.3.4] - 2025-10-02 - fixes a bug in content type in the forms of TD for HTTP protocol binding, when multiple serializers are used +- one can specify numpy array or arbitrary python objects in pydantic models for properties, actions and events ## [v0.3.3] - 2025-09-25