|
| 1 | +# Object Proxy |
| 2 | + |
| 3 | +[API Reference](../../api-reference/clients/object-proxy.md) |
| 4 | + |
| 5 | +`Thing` objects can be consumed using an `ObjectProxy` instance per protocol, where the interactions with |
| 6 | +a property, action or event are abstracted as operations like: |
| 7 | + |
| 8 | +- Read/Write/Observe Property |
| 9 | +- Invoke Action |
| 10 | +- Subscribe/Unsubscribe Event |
| 11 | + |
| 12 | +To instantiate an `ObjectProxy`, use the `ClientFactory`: |
| 13 | + |
| 14 | +=== "HTTP" |
| 15 | + |
| 16 | + ```py title="HTTP Client" linenums="1" hl_lines="7-10" |
| 17 | + from hololinked.client import ClientFactory |
| 18 | + |
| 19 | + thing = ClientFactory.http(url="http://localhost:8000/my-thing/resources/wot-td") |
| 20 | + ``` |
| 21 | + |
| 22 | + One needs to append `/resources/wot-td` to the URL to load a [Thing Description](https://www.w3.org/TR/wot-thing-description11/#introduction-td) |
| 23 | + of the `Thing`. The `Thing Description` contains the metadata of the `Thing` like available properties, actions and events, |
| 24 | + their data types, forms (protocols and endpoints) etc. which can be used to create the `ObjectProxy`. |
| 25 | + |
| 26 | +=== "ZMQ" |
| 27 | + |
| 28 | + ```py title="ZMQ Client" linenums="1" hl_lines="7-10" |
| 29 | + from hololinked.client import ClientFactory |
| 30 | + |
| 31 | + thing = ClientFactory.zmq(server_id="test-server", thing_id="my-thing", access_point="tcp://localhost:5555") |
| 32 | + ``` |
| 33 | + |
| 34 | + For `IPC`: |
| 35 | + |
| 36 | + ```py title="ZMQ Client IPC" linenums="1" hl_lines="7-10" |
| 37 | + thing = ClientFactory.zmq(server_id="test-server", thing_id="my-thing", access_point="IPC") |
| 38 | + ``` |
| 39 | + |
| 40 | + When using TCP, on the server side one may choose the address as `access_point="tcp://*:5555"`. |
| 41 | + On the client side, however, one must use the explicit address, like `access_point="tcp://my-pc:5555"` or |
| 42 | + `access_point="tcp://localhost:5555"`. |
| 43 | + |
| 44 | + The `Thing Description` is fetched automatically from the server. |
| 45 | + |
| 46 | + |
| 47 | +!!! Note |
| 48 | + |
| 49 | + Only one protocol is allowed per client. |
| 50 | + |
| 51 | +### read and write properties |
| 52 | + |
| 53 | +To read and write properties by name, one can use `read_property` and `write_property`, or the dot operator: |
| 54 | + |
| 55 | +```py title="read and write property" linenums="1" |
| 56 | +--8<-- "docs/beginners-guide/code/object_proxy/sync.py:8:17" |
| 57 | +``` |
| 58 | + |
| 59 | +To read and write multiple properties: |
| 60 | + |
| 61 | +```py title="read and write multiple properties" linenums="1" |
| 62 | +--8<-- "docs/beginners-guide/code/object_proxy/sync.py:48:55" |
| 63 | +``` |
| 64 | + |
| 65 | +### invoke actions |
| 66 | + |
| 67 | +One can also access actions with dot operator and supply positional and keyword arguments: |
| 68 | + |
| 69 | +```py title="invoke actions with dot operator" linenums="1" |
| 70 | +--8<-- "docs/beginners-guide/code/object_proxy/sync.py:21:30" |
| 71 | +``` |
| 72 | + |
| 73 | +One can also use `invoke_action` to invoke an action by name |
| 74 | + |
| 75 | +```py title="invoke_action()" linenums="1" |
| 76 | +--8<-- "docs/beginners-guide/code/object_proxy/sync.py:34:45" |
| 77 | +``` |
| 78 | + |
| 79 | +### oneway scheduling |
| 80 | + |
| 81 | +`oneway` scheduling do not fetch return value and exceptions that might occur while executing a property or an action. |
| 82 | +The server schedules the operation and returns an empty response to the client, allowing it to process further logic. |
| 83 | +It is possible to set a property, set multiple or all properties or invoke an action in |
| 84 | +oneway. Other operations are not supported. |
| 85 | + |
| 86 | +```py title="oneway=True" linenums="1" |
| 87 | +--8<-- "docs/beginners-guide/code/object_proxy/sync.py:58:73" |
| 88 | +``` |
| 89 | + |
| 90 | +Simply provide the keyword argument `oneway=True` to the operation method. |
| 91 | + |
| 92 | +Importantly, one cannot have an action argument or a property on the server named `oneway` as it is a |
| 93 | +reserved keyword argument to such methods on the client. At least they become inaccessible on the `ObjectProxy`. |
| 94 | + |
| 95 | +### no-block scheduling |
| 96 | + |
| 97 | +`noblock` allows scheduling a property or action but collecting the reply later: |
| 98 | + |
| 99 | +```py title="noblock=True" linenums="1" |
| 100 | +--8<-- "docs/beginners-guide/code/object_proxy/sync.py:77:101" |
| 101 | +``` |
| 102 | + |
| 103 | +When using `read_reply()`, `noblock` calls raise exception on the client if the server raised its own exception or fetch the return value . |
| 104 | + |
| 105 | +<!-- We supported this before, but not now, TODO renable --> |
| 106 | +<!-- Timeout exceptions are raised when there is no reply within timeout specified. |
| 107 | +
|
| 108 | +.. literalinclude:: code/object_proxy/sync.py |
| 109 | + :language: python |
| 110 | + :linenos: |
| 111 | + :lines: 95-98 --> |
| 112 | + |
| 113 | +!!! Note |
| 114 | + |
| 115 | + One cannot combine `oneway` and `noblock` - `oneway` takes precedence over `noblock`. |
| 116 | + |
| 117 | +### async client-side scheduling |
| 118 | + |
| 119 | +All operations on the `ObjectProxy` can also be invoked in an asynchronous manner within an `async` function. |
| 120 | +Simply prefix `async_` to the method name, like `async_read_property`, `async_write_property`, `async_invoke_action` etc.: |
| 121 | + |
| 122 | +```py title="asyncio" linenums="1" |
| 123 | +--8<-- "docs/beginners-guide/code/object_proxy/async.py:12:28" |
| 124 | +``` |
| 125 | + |
| 126 | +There is no support for dot operator based access. One may also note that `async` operations |
| 127 | +do not change the nature of the execution on the server side. |
| 128 | +`asyncio` on `ObjectProxy` is purely a client-side non-blocking network call, so that one can |
| 129 | +simultaneously perform other (async) operations while the client is waiting for the network operation to complete. |
| 130 | + |
| 131 | +!!! Note |
| 132 | + |
| 133 | + `oneway` and `noblock` are not supported for async calls due to the asynchronous nature of the |
| 134 | + operation themselves. |
| 135 | + |
| 136 | +### subscribe and unsubscribe events |
| 137 | + |
| 138 | +To subscribe to an event, use `subscribe_event` method and pass a callback function that accepts a single argument, the event data: |
| 139 | + |
| 140 | +```py title="subscribe_event()" linenums="1" |
| 141 | +from hololinked.client.abstractions import SSE |
| 142 | + |
| 143 | +def update_plot(event: SSE): |
| 144 | + plt.clf() # Clear the current figure |
| 145 | + plt.plot(x_axis, event.data["spectrum"], color='red', linewidth=2) |
| 146 | + plt.title(f'Live Spectrum - {event.data["timestamp"]} UTC') |
| 147 | + # assuming event data is a dictionary with keys spectrum and timestamp |
| 148 | + |
| 149 | +spectrometer.subscribe_event("intensity_measurement_event", callbacks=update_plot) |
| 150 | +``` |
| 151 | + |
| 152 | +To unsubscribe from an event, use `unsubscribe_event` method: |
| 153 | + |
| 154 | +```py title="unsubscribe_event()" linenums="1" |
| 155 | +spectrometer.unsubscribe_event("intensity_measurement_event") |
| 156 | +``` |
| 157 | + |
| 158 | +One can also supply multiple callbacks to be executed in series or concurrently, schedule an async callback etc., see [events section](./events.md#subscription) for further details. |
| 159 | + |
| 160 | +### customizations |
| 161 | + |
| 162 | +##### foreign attributes on client |
| 163 | + |
| 164 | +Normally, there cannot be user defined attributes on the `ObjectProxy` as the attributes on the client |
| 165 | +must mimic the available properties, actions and events on the server. An accidental setting of an unknown |
| 166 | +property must raise an `AttributeError` when not found on the server, instead of silently going through and setting |
| 167 | +said property on the client object itself: |
| 168 | + |
| 169 | +```py title="foreign attributes raise AttributeError" linenums="1" |
| 170 | +--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:3:5" |
| 171 | +``` |
| 172 | + |
| 173 | +One can overcome this by setting `allow_foreign_attributes` to `True`: |
| 174 | + |
| 175 | +```py title="foreign attributes allowed" linenums="1" |
| 176 | +--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:7:12" |
| 177 | +``` |
| 178 | + |
| 179 | +##### controlling timeouts for non-responsive server |
| 180 | + |
| 181 | +For invoking any operation (say property read/write & action call), two types of timeouts can be configured: |
| 182 | + |
| 183 | +- `invokation_timeout` - the amount of time the server has to wait for an operation to be scheduled |
| 184 | +- `execution_timeout` - the amount of time the server has to complete the operation once scheduled |
| 185 | + |
| 186 | +When the `invokation_timeout` expires, the operation is guaranteed to be never scheduled. When the `execution_timeout` expires, the operation is scheduled but returns without the expected response. In both cases, a `TimeoutError` is raised on the client side specifying the timeout type. If an operation is scheduled but not completed within the `execution_timeout`, the server may still complete the operation and there can be unknown side effects or client does not know about it. |
| 187 | + |
| 188 | +```py title="timeout specification" linenums="1" |
| 189 | +--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:20:27" |
| 190 | +``` |
| 191 | + |
| 192 | +!!! Note |
| 193 | + |
| 194 | + Currently only a global specification is supported. In future, one may be able to specify timeouts per operation. |
| 195 | + |
| 196 | + |
| 197 | +<!-- #### change handshake timeout |
| 198 | +
|
| 199 | +Before sending the first message to the server, a handshake is always done explicitly to not loose messages on the socket. |
| 200 | +This is an artificat of ZMQ (which also does its own handshake). `handshake_timeout` controls how long to look for the server, |
| 201 | +in case the server takes a while to boot. |
| 202 | +
|
| 203 | +```py title="timeout" linenums="1" |
| 204 | +--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:10:11" |
| 205 | +``` |
| 206 | +
|
| 207 | +Default value is 1 minute. A `ConnectionError` is raised if the server cannot be contacted. |
| 208 | +
|
| 209 | +One can also delay contacting the server by setting `load_thing` to False. But one has to manually performing the `handshake` later |
| 210 | +before loading the server resources: |
| 211 | +
|
| 212 | +```py title="timeout" linenums="1" |
| 213 | +--8<-- "docs/beginners-guide/code/object_proxy/customizations.py:12:17" |
| 214 | +``` |
| 215 | +
|
| 216 | +If one is completely sure that server is online, one may drop the manual handshake. --> |
| 217 | + |
0 commit comments