Skip to content

Commit e55ae27

Browse files
committed
Documentation on Thing, and added a link to the TD model.
1 parent fcfb6b4 commit e55ae27

8 files changed

Lines changed: 140 additions & 58 deletions

File tree

NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ switched to using `pydoclint` directly, and configured it in `pyproject.toml`. I
6969
- `introspection`:
7070
- There's a confusing TODO about path parameters in `fastapi_dependency_params`
7171
- There's a ValueError that might want subclassing in `input_model_from_signature`.
72+
- `exceptions` will need to hoover up more exceptions. Do we define them here? probably yes...
73+
* `notifications` is empty - need to consolidate code from property/action/websocket.
74+
* `thing`:
75+
- consolidate settings into an object?
76+
- default `thing_state` does cacheing but this isn't really documented. Remove?
77+
- thing_description should consolidate `path` and `base_url`. In fact, if we set `base_url` to be the path
78+
to the TD, we can make everything else static.
79+
* General: there are a lot of class attributes/annotations that should maybe be in `__init__`. We need to pick a convention and stick to it, I have often defined class attrs next to the function(s) that use them, but that might be bad style?
7280

7381

7482

docs/source/lt_core_concepts.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _labthings_cc
2+
13
LabThings Core Concepts
24
=======================
35

docs/source/wot_core_concepts.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ A `Thing` represents a piece of hardware or software. It could be a whole instru
1414

1515
`labthings-fastapi` automatically generates a `Thing Description`_ to describe each `Thing`. Each function offered by the `Thing` is either a Property, Action, or Event. These are termed "interaction affordances" in WoT_ terminology.
1616

17-
.. _wot_properties
17+
.. _wot_properties:
1818

1919
Properties
2020
----------
@@ -54,4 +54,11 @@ Thing Description documents are higher-level than OpenAPI_ and focus on the capa
5454

5555
.. _WoT: https://www.w3.org/WoT/
5656
.. _Thing Description: https://www.w3.org/TR/wot-thing-description/
57-
.. _OpenAPI: https://www.openapis.org/
57+
.. _OpenAPI: https://www.openapis.org/
58+
59+
.. _wot_affordances:
60+
61+
Interaction Affordances
62+
-----------------------
63+
64+
The Web of Things standard often talks about Affordances. This is the collective term for _wot_properties, _wot_actions, and _wot_events.

src/labthings_fastapi/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
"""A submodule for custom LabThings-FastAPI Exceptions"""
1+
"""A submodule for custom LabThings-FastAPI Exceptions."""
22

33
from .dependencies.invocation import InvocationCancelledError
44

55

66
class NotConnectedToServerError(RuntimeError):
7-
"""The Thing is not connected to a server
7+
"""The Thing is not connected to a server.
88
99
This exception is called if a ThingAction is called or
1010
is a ThingProperty is updated on a Thing that is not

src/labthings_fastapi/notifications.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
Handle notification of events, property, and action status changes
1+
"""Handle notification of events, property, and action status changes.
32
43
There are several kinds of "event" in the WoT vocabulary, not all of which
54
are called Event, which is why this module is called `notifications`.
@@ -20,4 +19,6 @@
2019

2120

2221
class Listener:
22+
"""A placeholder class for objects that listen for notifications."""
23+
2324
pass

src/labthings_fastapi/thing.py

Lines changed: 110 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
"""
2-
The `Thing` class enables most of the functionality of this library,
3-
and is the way in to most of its features. In the future, we might
4-
support a stub version of the class in a separate package, so
5-
that instrument control libraries can be LabThings compatible
6-
without a hard dependency on LabThings. But that is something we
7-
will do in the future...
1+
"""A class to represent hardware or software Things.
2+
3+
The `.Thing` class enables most of the functionality of this library,
4+
and is the way in to most of its features. See wot_cc_ and labthings_cc_
5+
for more.
86
"""
97

108
from __future__ import annotations
11-
from typing import TYPE_CHECKING, Optional
9+
from typing import TYPE_CHECKING, Any, Optional, Self
1210
from collections.abc import Mapping
1311
import logging
1412
import os
@@ -44,67 +42,85 @@ class Thing:
4442
a particular function - it will correspond to a path on the server, and a Thing
4543
Description document.
4644
47-
## Subclassing Notes
45+
Subclassing Notes
46+
-----------------
4847
49-
* `__init__`: You should accept any arguments you need to configure the Thing
50-
in `__init__`. Don't initialise any hardware at this time, as your Thing may
48+
* ``__init__``: You should accept any arguments you need to configure the Thing
49+
in ``__init__``. Don't initialise any hardware at this time, as your Thing may
5150
be instantiated quite early, or even at import time.
52-
* `__enter__(self)` and `__exit__(self, exc_t, exc_v, exc_tb)` are where you
51+
* ``__enter__(self)`` and ``__exit__(self, exc_t, exc_v, exc_tb)`` are where you
5352
should start and stop communications with the hardware. This is Python's standard
54-
"context manager" protocol. The arguments of `__exit__` will be `None` unless
53+
"context manager" protocol. The arguments of ``__exit__`` will be ``None`` unless
5554
an exception has occurred. You should be safe to ignore them, and just include
56-
code that will close down your hardware. It's equivalent to a `finally:` block.
57-
* Properties and Actions are defined using decorators: the `@thing_action` decorator
58-
declares a method to be an action, which will run when it's triggered, and the
59-
`@thing_property` decorator (or `ThingProperty` descriptor) does the same for
60-
a property. See the documentation on those functions for more detail.
55+
code that will close down your hardware. It's equivalent to a ``finally`:` block.
56+
* Properties and Actions are defined using decorators: the :deco:`.thing_action`
57+
decorator declares a method to be an action, which will run when it's triggered,
58+
and the :deco:`.thing_property` decorator (or `.ThingProperty` descriptor) does
59+
the same for a property. See the documentation on those functions for more
60+
detail.
6161
* `title` will be used in various places as the human-readable name of your Thing,
6262
so it makes sense to set this in a subclass.
6363
64-
There are various LabThings methods that you should avoid overriding unless you know
65-
what you are doing: anything not mentioned above that's defined in `Thing` is
66-
probably best left along. They may in time be collected together into a single
64+
There are various LabThings methods that you should avoid overriding unless you
65+
know what you are doing: anything not mentioned above that's defined in `Thing` is
66+
probably best left alone. They may in time be collected together into a single
6767
object to avoid namespace clashes.
6868
"""
6969

7070
title: str
71+
"""A human-readable description of the Thing"""
7172
_labthings_blocking_portal: Optional[BlockingPortal] = None
73+
"""See concurrency_ for why blocking portal is needed."""
7274
path: Optional[str]
75+
"""The path at which the `.Thing` is exposed over HTTP."""
7376

74-
async def __aenter__(self):
77+
async def __aenter__(self) -> Self:
7578
"""Context management is used to set up/close the thing.
7679
7780
As things (currently) do everything with threaded code, we define
78-
async __aenter__ and __aexit__ wrappers to call the synchronous
81+
async ``__aenter__`` and ``__aexit__`` wrappers to call the synchronous
7982
code, if it exists.
83+
84+
:return: this object.
8085
"""
8186
if hasattr(self, "__enter__"):
8287
return await run_sync(self.__enter__)
8388
else:
8489
return self
8590

86-
async def __aexit__(self, exc_t, exc_v, exc_tb):
91+
async def __aexit__(
92+
self, exc_t: type[Exception] | None, exc_v: Exception | None, exc_tb: Any
93+
) -> None:
8794
"""Wrap context management functions, if they exist.
8895
89-
See __aenter__ docs for more details.
96+
See ``__aenter__`` for more details.
97+
98+
:param exc_t: The type of the exception, or ``None``.
99+
:param exc_v: The exception that occurred, or ``None``.
100+
:param exc_tb: The traceback for the exception, or ``None``.
90101
"""
91102
if hasattr(self, "__exit__"):
92-
return await run_sync(self.__exit__, exc_t, exc_v, exc_tb)
103+
await run_sync(self.__exit__, exc_t, exc_v, exc_tb)
93104

94105
def attach_to_server(
95106
self, server: ThingServer, path: str, setting_storage_path: str
96-
):
97-
"""Attatch this thing to the server.
107+
) -> None:
108+
"""Attach this thing to the server.
98109
99110
Things need to be attached to a server before use to function correctly.
100111
101-
:param server: The server to attach this Thing to
102-
:param settings_storage_path: The path on disk to save the any Thing Settings
112+
:param server: The server to attach this Thing to.
113+
:param path: The root URL for the Thing.
114+
:param setting_storage_path: The path on disk to save the any Thing Settings
103115
to. This should be the path to a json file. If it does not exist it will be
104116
created.
105117
106-
Wc3 Web Of Things explanation:
107-
This will add HTTP handlers to an app for all Interaction Affordances
118+
Attaching the `.Thing` to a `.ThingServer` allows the `.Thing` to start
119+
actions, load its settings from the correct place, and create HTTP endpoints
120+
to allow it to be accessed from the HTTP API.
121+
122+
We create HTTP endpoints for all wot_affordances_ on the `.Thing`, as well
123+
as any `.EndpointDescriptor` descriptors.
108124
"""
109125
self.path = path
110126
self.action_manager: ActionManager = server.action_manager
@@ -139,7 +155,7 @@ async def websocket(ws: WebSocket):
139155

140156
@property
141157
def _settings(self) -> Optional[dict[str, ThingSetting]]:
142-
"""A private property that returns a dict of all settings for this Thing
158+
"""A private property that returns a dict of all settings for this Thing.
143159
144160
Each dict key is the name of the setting, the corresponding value is the
145161
ThingSetting class (a descriptor). This can be used to directly get the
@@ -159,11 +175,31 @@ def _settings(self) -> Optional[dict[str, ThingSetting]]:
159175

160176
@property
161177
def setting_storage_path(self) -> Optional[str]:
162-
"""The storage path for settings. This is set as the Thing is added to a server"""
178+
"""The storage path for settings.
179+
180+
.. note::
181+
182+
This is set in `.Thing.attach_to_server`. It is ``None`` during the
183+
``__init__`` method, so it is best to avoid using settings until the
184+
`.Thing` is set up in ``__enter__``.
185+
"""
163186
return self._setting_storage_path
164187

165-
def load_settings(self, setting_storage_path):
166-
"""Load settings from json. This is run when the Thing is added to a server"""
188+
def load_settings(self, setting_storage_path: str) -> None:
189+
"""Load settings from json.
190+
191+
Read the JSON file and use it to populate settings.
192+
193+
.. note::
194+
Settings are loaded when the Thing is added to a server, so they will
195+
not be available while the ``__init__`` method is run.
196+
197+
Note that no notifications will be triggered when the settings are set,
198+
so if action is needed (e.g. updating hardware with the loaded settings)
199+
it should be taken in ``__enter__``.
200+
201+
:param setting_storage_path: The path where the settings should be stored.
202+
"""
167203
# Ensure that the settings path isn't set during loading or saving will be triggered
168204
self._setting_storage_path = None
169205
thing_name = type(self).__name__
@@ -185,7 +221,11 @@ def load_settings(self, setting_storage_path):
185221
self._setting_storage_path = setting_storage_path
186222

187223
def save_settings(self):
188-
"""Save settings to JSON. This is called whenever a setting is updated"""
224+
"""Save settings to JSON.
225+
226+
This is called whenever a setting is updated. All settings are written to
227+
the settings file every time.
228+
"""
189229
if self._settings is not None:
190230
setting_dict = {}
191231
for name in self._settings.keys():
@@ -202,7 +242,7 @@ def save_settings(self):
202242

203243
@property
204244
def thing_state(self) -> Mapping:
205-
"""Return a dictionary summarising our current state
245+
"""Return a dictionary summarising our current state.
206246
207247
This is intended to be an easy way to collect metadata from a Thing that
208248
summarises its state. It might be used, for example, to record metadata
@@ -219,8 +259,8 @@ def thing_state(self) -> Mapping:
219259
self._labthings_thing_state = {}
220260
return self._labthings_thing_state
221261

222-
def validate_thing_description(self):
223-
"""Raise an exception if the thing description is not valid"""
262+
def validate_thing_description(self) -> None:
263+
"""Raise an exception if the thing description is not valid."""
224264
td = self.thing_description_dict()
225265
return validation.validate_thing_description(td)
226266

@@ -231,12 +271,17 @@ def validate_thing_description(self):
231271
def thing_description(
232272
self, path: Optional[str] = None, base: Optional[str] = None
233273
) -> ThingDescription:
234-
"""A w3c Thing Description representing this thing
274+
"""Generate a w3c Thing Description representing this thing.
235275
236276
The w3c Web of Things working group defined a standard representation
237277
of a Thing, which provides a high-level description of the actions,
238278
properties, and events that it exposes. This endpoint delivers a JSON
239-
representation of the Thing Description for this Thing.
279+
representation of the wot_td_ for this Thing.
280+
281+
:param path: the URL pointing to this Thing.
282+
:param base: the base URL for all URLs in the thing description.
283+
284+
:return: a Thing Description.
240285
"""
241286
path = path or getattr(self, "path", "{base_uri}")
242287
if (
@@ -270,26 +315,41 @@ def thing_description_dict(
270315
path: Optional[str] = None,
271316
base: Optional[str] = None,
272317
) -> dict:
273-
"""A w3c Thing Description representing this thing, as a simple dict
318+
r"""Describe this Thing with a Thing Description as a simple dict.
274319
275-
The w3c Web of Things working group defined a standard representation
276-
of a Thing, which provides a high-level description of the actions,
277-
properties, and events that it exposes. This endpoint delivers a JSON
278-
representation of the Thing Description for this Thing.
320+
See `.Thing.thing_description`\ . This function converts the
321+
return value of that function into a simple dictionary.
322+
323+
:param path: the URL pointing to this Thing.
324+
:param base: the base URL for all URLs in the thing description.
325+
326+
:return: a Thing Description.
279327
"""
280328
td: ThingDescription = self.thing_description(path=path, base=base)
281329
td_dict: dict = td.model_dump(exclude_none=True, by_alias=True)
282330
return jsonable_encoder(td_dict)
283331

284-
def observe_property(self, property_name: str, stream: ObjectSendStream):
285-
"""Register a stream to receive property change notifications"""
332+
def observe_property(self, property_name: str, stream: ObjectSendStream) -> None:
333+
"""Register a stream to receive property change notifications.
334+
335+
:param property_name: the property to register for.
336+
:param stream: the stream used to send events.
337+
338+
:raises KeyError: if the requested name is not defined on this Thing.
339+
"""
286340
prop = getattr(self.__class__, property_name)
287341
if not isinstance(prop, ThingProperty):
288342
raise KeyError(f"{property_name} is not a LabThings Property")
289343
prop._observers_set(self).add(stream)
290344

291345
def observe_action(self, action_name: str, stream: ObjectSendStream):
292-
"""Register a stream to receive action status change notifications"""
346+
"""Register a stream to receive action status change notifications.
347+
348+
:param action_name: the action to register for.
349+
:param stream: the stream used to send events.
350+
351+
:raises KeyError: if the requested name is not defined on this Thing.
352+
"""
293353
action = getattr(self.__class__, action_name)
294354
if not isinstance(action, ActionDescriptor):
295355
raise KeyError(f"{action_name} is not an LabThings Action")

src/labthings_fastapi/thing_description/_model.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
33
wot_td_ defines a schema for describing Things, using JSONSchema-like syntax
44
for data types. This file contains pydantic models that describe that
5-
schema. For the meaning of the various objects, please refer to the schema
6-
definition within the W3C standard.
5+
schema. For the meaning of the various objects, please refer to the
6+
td_schema_definition_ within the W3C standard.
7+
8+
.. _td_schema_definition: https://www.w3.org/TR/wot-thing-description11/
79
810
This file was automatically generated, but has been customised to improve the
911
types and simplify/combine objects.

src/labthings_fastapi/thing_description/validation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
validation applied every time one is created. However, this module allows
55
the generated JSON document to be formally validated against the schema
66
in the W3C specification, as an additional check.
7+
8+
See wot_td_ for a link to the specification in human-readable format.
79
"""
810

911
from importlib.resources import files

0 commit comments

Comments
 (0)