Skip to content

v0.2.11 #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ on:
pull_request:
branches:
- main
- main-next-release
push:
branches:
- main
- main-next-release

jobs:
test:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ This release will contain a lot of new features and improvements so that a versi
Not finalised:
- cookie auth & its specification in TD (cookie auth branch)

## [v0.2.11] - 2025-04-25

- new feature - support for JSON files as backup for property values (use with `db_commit`, `db_persist` and `db_init`). Compatible only with JSON serializable properties.

## [v0.2.10] - 2025-04-05

- bug fixes to support `class_member` properties to work with `fget`, `fset` and `fdel` methods. While using custom `fget`, `fset` and `fdel` methods for `class_member`s,
Expand Down
115 changes: 98 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ For those that understand, this package is a ZMQ/HTTP-RPC.
<br>
[![PyPI - Downloads](https://img.shields.io/pypi/dm/hololinked?label=pypi%20downloads)](https://pypistats.org/packages/hololinked)
[![Conda Downloads](https://img.shields.io/conda/d/conda-forge/hololinked)](https://anaconda.org/conda-forge/hololinked)
<br>
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15155942.svg)](https://doi.org/10.5281/zenodo.12802841)
[![Discord](https://img.shields.io/discord/1265289049783140464?label=Discord%20Members&logo=discord)](https://discord.com/invite/kEz87zqQXh)

### To Install

Expand Down Expand Up @@ -307,22 +310,104 @@ Here one can see the use of `instance_name` and why it turns up in the URL path.

##### NOTE - The package is under active development. Contributors welcome, please check CONTRIBUTING.md and the open issues. Some issues can also be independently dealt without much knowledge of this package.

- [example repository](https://github.com/VigneshVSV/hololinked-examples) - detailed examples for both clients and servers
- [helper GUI](https://github.com/VigneshVSV/thing-control-panel) - view & interact with your object's actions, properties and events.
- [examples repository](https://github.com/hololinked-dev/examples) - detailed examples for both clients and servers
- [helper GUI](https://github.com/hololinked-dev/thing-control-panel) - view & interact with your object's actions, properties and events.
- [live demo](https://control-panel.hololinked.dev/#https://examples.hololinked.dev/simulations/oscilloscope/resources/wot-td) - an example of an oscilloscope available for live test

See a list of currently supported possibilities while using this package [below](#currently-supported).

> You may use a script deployment/automation tool to remote stop and start servers, in an attempt to remotely control your hardware scripts.

### A little more about Usage
### Using APIs and Thing Descriptions

The HTTP API may be autogenerated or adjusted by the user. If your plan is to develop a truly networked system, it is recommended to learn more and
use [Thing Descriptions](https://www.w3.org/TR/wot-thing-description11) to describe your hardware (This is optional and one can still use a classic HTTP client). A Thing Description will be automatically generated if absent as shown in JSON examples above or can be supplied manually. The default end point to fetch thing descriptions are: <br> `http(s)://<host name>/<instance name of the thing>/resources/wot-td` <br>
If there are errors in generation of Thing Description
(mostly due to JSON non-complaint types), one could use: <br> `http(s)://<host name>/<instance name of the thing>/resources/wot-td?ignore_errors=true`
use [Thing Descriptions](https://www.w3.org/TR/wot-thing-description11) to describe your hardware (This is optional and one can still use a classic HTTP client). A Thing Description will be automatically generated if absent as shown in JSON examples above or can be supplied manually. The default end point to fetch thing descriptions are:

```
http(s)://<host-name>/<instance-name-of-the-thing>/resources/wot-td
http(s)://<host-name>/<instance-name-of-the-thing>/resources/wot-td?ignore_errors=true
```

If there are errors in generation of Thing Description (mostly due to JSON non-complaint types), use the second endpoint which may generate at least a partial but useful Thing Description.

### Consuming Thing Descriptions using node-wot (Javascript)

The Thing Descriptions (TDs) can be consumed with Web of Things clients like [node-wot](https://github.com/eclipse-thingweb/node-wot). Suppose an example TD for a device instance named `spectrometer` is available at the following endpoint:

```
http://localhost:8000/spectrometer/resources/wot-td
```

(client docs will be updated here next, also check official docs)
Consume this TD in a Node.js script using Node-WoT:

```js
const { Servient } = require("@node-wot/core");
const HttpClientFactory = require("@node-wot/binding-http").HttpClientFactory;

const servient = new Servient();
servient.addClientFactory(new HttpClientFactory());

servient.start().then((WoT) => {
fetch("http://localhost:8000/spectrometer/resources/wot-td")
.then((res) => res.json())
.then((td) => WoT.consume(td))
.then((thing) => {
thing.readProperty("integration_time").then(async(interactionOutput) => {
console.log("Integration Time: ", await interactionOutput.value());
})
)});
```
This works with both `http://` and `https://` URLs. If you're using HTTPS, just make sure the server certificate is valid or trusted by the client.

```js
const HttpsClientFactory = require("@node-wot/binding-http").HttpsClientFactory;
servient.addClientFactory(new HttpsClientFactory({ allowSelfSigned : true }))
```
You can see an example [here](https://gitlab.com/hololinked/examples/clients/node-clients/phymotion-controllers-app/-/blob/main/src/App.tsx?ref_type=heads#L77).

After consuming the TD, you can:

<details open>
<summary>Read Property</summary>

`thing.readProperty("integration_time").then(async(interactionOutput) => {
console.log("Integration Time:", await interactionOutput.value());
});`
</details>
<details open>
<summary>Write Property</summary>

`thing.writeProperty("integration_time", 2000).then(() => {
console.log("Integration Time updated");
});`
</details>
<details open>
<summary>Invoke Action</summary>

`thing.invokeAction("connect", { serial_number: "S14155" }).then(() => {
console.log("Device connected");
});`
</details>
<details open>
<summary>Subscribe to Event</summary>

`thing.subscribeEvent("intensity_measurement_event", async (interactionOutput) => {
console.log("Received event:", await interactionOutput.value());
});`
</details>

Try out the above code snippets with an online example [using this TD](http://examples.hololinked.net/simulations/spectrometer/resources/wot-td).
> Note: due to reverse proxy buffering, subscribeEvent may take up to 1 minute to receive data. All other operations work fine.

In React, the Thing Description may be fetched inside `useEffect` hook, the client passed via `useContext` hook and the individual operations can be performed in their own callbacks attached to user elements.
<details>
<summary>Links to Examples</summary>
For React examples using Node-WoT, refer to:

- [example1](https://gitlab.com/hololinked/examples/clients/node-clients/phymotion-controllers-app/-/blob/main/src/App.tsx?ref_type=heads#L96)
- [example2](https://gitlab.com/hololinked/examples/clients/node-clients/phymotion-controllers-app/-/blob/main/src/components/movements.tsx?ref_type=heads#L54)
</details>

### Currently Supported

- control method execution and property write with a custom finite state machine.
Expand All @@ -335,15 +420,11 @@ If there are errors in generation of Thing Description
- run direct ZMQ-TCP server without HTTP details
- serve multiple objects with the same HTTP server, run HTTP Server & python object in separate processes or the same process

Again, please check examples or the code for explanations. Documentation is being activety improved.

### Currently being worked

- unit tests coverage
- separation of HTTP protocol specification like URL path and HTTP verbs from the API of properties, actions and events and move their customization completely to the HTTP server
- serve multiple things with the same server (unfortunately due to a small oversight it is currently somewhat difficult for end user to serve multiple things with the same server, although its possible. This will be fixed.)
- improving accuracy of Thing Descriptions
- cookie credentials for authentication - as a workaround until credentials are supported, use `allowed_clients` argument on HTTP server which restricts access based on remote IP supplied with the HTTP headers. This wont still help you in public networks or modified/non-standard HTTP clients.

Again, please check examples or the code for explanations. Documentation is being actively improved.

### Contributing

See [organization info](https://github.com/hololinked-dev) for details regarding contributing to this package. There is:
- [discord group](https://discord.com/invite/kEz87zqQXh)
- [weekly meetings](https://github.com/hololinked-dev/#monthly-meetings) and
- [project planning](https://github.com/orgs/hololinked-dev/projects/4) to discuss activities around this repository.
2 changes: 1 addition & 1 deletion doc
Submodule doc updated from c0c4a8 to d4e965
2 changes: 1 addition & 1 deletion examples
2 changes: 1 addition & 1 deletion hololinked/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.10"
__version__ = "0.2.11"
15 changes: 11 additions & 4 deletions hololinked/param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,10 @@ def __set__(self, obj : typing.Union['Parameterized', typing.Any], value : typin
raise_ValueError("Read-only parameter cannot be set/modified.", self)

value = self.validate_and_adapt(value)

if self.class_member and obj is not self.owner: # safety check

if not self.class_member and obj is self.owner:
raise AttributeError("Cannot set instance parameter on class")
if self.class_member and obj is not self.owner:
obj = self.owner

old = NotImplemented
Expand Down Expand Up @@ -1807,8 +1809,13 @@ def __setattr__(mcs, attribute_name : str, value : typing.Any) -> None:
if attribute_name != '_param_container' and attribute_name != '__%s_params__' % mcs.__name__:
parameter = mcs.parameters.descriptors.get(attribute_name, None)
if parameter: # and not isinstance(value, Parameter):
parameter.__set__(mcs, value)
return
try:
parameter.__set__(mcs, value)
return
except AttributeError as ex:
# raised for class attribute
if not str(ex).startswith("Cannot set instance parameter on class"):
raise ex from None
return type.__setattr__(mcs, attribute_name, value)

def __getattr__(mcs, attribute_name : str) -> typing.Any:
Expand Down
165 changes: 165 additions & 0 deletions hololinked/server/json_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import os
import threading
from typing import Any, Dict, List, Optional, Union
from .serializers import JSONSerializer
from .property import Property
from ..param import Parameterized


class ThingJsonStorage:
"""
JSON-based storage engine composed within ``Thing``. Carries out property operations such as storing and
retrieving values from a plain JSON file.

Parameters
----------
filename : str
Path to the JSON file to use for storage.
instance : Parameterized
The ``Thing`` instance which uses this storage. Required to read default property values when
creating missing properties.
serializer : JSONSerializer, optional
Serializer used for encoding and decoding JSON data. Defaults to an instance of ``JSONSerializer``.
"""
def __init__(self, filename: str, instance: Parameterized, serializer: Optional[Any]=None):
self.filename = filename
self.thing_instance = instance
self.instance_name = instance.instance_name
self._serializer = serializer or JSONSerializer()
self._lock = threading.RLock()
self._data = self._load()

def _load(self) -> Dict[str, Any]:
"""
Load and decode data from the JSON file.

Returns
-------
value: dict
A dictionary of all stored properties. Empty if the file does not exist or cannot be decoded.
"""
if not os.path.exists(self.filename) or os.path.getsize(self.filename) == 0:
return {}
try:
with open(self.filename, 'rb') as f:
raw_bytes = f.read()
if not raw_bytes:
return {}
return self._serializer.loads(raw_bytes)
except Exception:
return {}

def _save(self):
"""
Encode and write data to the JSON file.
"""
raw_bytes = self._serializer.dumps(self._data)
with open(self.filename, 'wb') as f:
f.write(raw_bytes)

def get_property(self, property: Union[str, Property]) -> Any:
"""
Fetch a single property.

Parameters
----------
property: str | Property
string name or descriptor object

Returns
-------
value: Any
property value
"""
name = property if isinstance(property, str) else property.name
if name not in self._data:
raise KeyError(f"property {name} not found in JSON storage")
with self._lock:
return self._data[name]

def set_property(self, property: Union[str, Property], value: Any) -> None:
"""
change the value of an already existing property.

Parameters
----------
property: str | Property
string name or descriptor object
value: Any
value of the property
"""
name = property if isinstance(property, str) else property.name
with self._lock:
self._data[name] = value
self._save()

def get_properties(self, properties: Dict[Union[str, Property], Any]) -> Dict[str, Any]:
"""
get multiple properties at once.

Parameters
----------
properties: List[str | Property]
string names or the descriptor of the properties as a list

Returns
-------
value: Dict[str, Any]
property names and values as items
"""
names = [key if isinstance(key, str) else key.name for key in properties.keys()]
with self._lock:
return {name: self._data.get(name) for name in names}

def set_properties(self, properties: Dict[Union[str, Property], Any]) -> None:
"""
change the values of already existing few properties at once

Parameters
----------
properties: Dict[str | Property, Any]
string names or the descriptor of the property and any value as dictionary pairs
"""
with self._lock:
for obj, value in properties.items():
name = obj if isinstance(obj, str) else obj.name
self._data[name] = value
self._save()

def get_all_properties(self) -> Dict[str, Any]:
"""
read all properties of the ``Thing`` instance.
"""
with self._lock:
return dict(self._data)

def create_missing_properties(self, properties: Dict[str, Property],
get_missing_property_names: bool = False) -> Optional[List[str]]:
"""
create any and all missing properties of ``Thing`` instance

Parameters
----------
properties: Dict[str, Property]
descriptors of the properties

Returns
-------
missing_props: List[str]
list of missing properties if get_missing_property_names is True
"""
missing_props = []
with self._lock:
existing_props = self.get_all_properties()
for name, new_prop in properties.items():
if name not in existing_props:
self._data[name] = getattr(self.thing_instance, new_prop.name)
missing_props.append(name)
self._save()
if get_missing_property_names:
return missing_props


__all__ = [
ThingJsonStorage.__name__,
]
2 changes: 1 addition & 1 deletion hololinked/server/td.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import typing, inspect
from dataclasses import dataclass, field

from hololinked.server.eventloop import EventLoop


from .constants import JSON, JSONSerializable
Expand All @@ -12,6 +11,7 @@
from .property import Property
from .thing import Thing
from .state_machine import StateMachine
from .eventloop import EventLoop



Expand Down
Loading
Loading