Skip to content

Commit 9bc400d

Browse files
committed
complete actions, only checking pending
1 parent 997b847 commit 9bc400d

File tree

3 files changed

+150
-11
lines changed

3 files changed

+150
-11
lines changed

docs/beginners-guide/articles/actions.md

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ on the robustness the developer is expecting in their application:
202202
class Picoscope6000(Thing):
203203

204204
@action()
205-
def set_channel_pydantic(
205+
def set_channel(
206206
self,
207207
channel: Literal["A", "B", "C", "D"],
208208
enabled: bool = True,
@@ -229,6 +229,10 @@ on the robustness the developer is expecting in their application:
229229

230230
???+ note "JSON schema seen in Thing Description"
231231

232+
```py
233+
Picoscope6000.set_channel.to_affordance().json()
234+
```
235+
232236
```json
233237
{
234238
"description": "Set the parameter for a channel. https://www.picotech.com/download/manuals/picoscope-6000-series-a-api-programmers-guide.pdf",
@@ -278,13 +282,54 @@ on the robustness the developer is expecting in their application:
278282
}
279283
```
280284

285+
=== "Return Type"
286+
287+
```py
288+
from typing import Annotated
289+
from pydantic import Field
290+
291+
class SerialUtility(Thing):
292+
293+
@action()
294+
def execute_instruction(
295+
self, command: str, return_data_size: Annotated[int, Field(ge=0)] = 0
296+
) -> str:
297+
"""
298+
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.
299+
"""
300+
```
301+
302+
???+ note "JSON schema seen in Thing Description"
303+
304+
```py
305+
SerialUtility.execute_instruction.to_affordance().json()
306+
```
307+
308+
```json
309+
{
310+
"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.",
311+
"input": {
312+
"properties": {
313+
"command": {"type": "string"},
314+
"return_data_size": {"default": 0, "minimum": 0, "type": "integer"}
315+
},
316+
"required": ["command"],
317+
"type": "object"
318+
},
319+
"output": {"type": "string"},
320+
"synchronous": True
321+
}
322+
```
323+
281324
However, a schema is optional and it only matters that
282-
the method signature is matching when requested from a client. To enable this, set global attribute `allow_relaxed_schema_actions=True`. This setting is used especially when a schema is useful for validation of arguments but not available - not for methods with no arguments.
325+
the method signature is matching when requested from a client.
326+
327+
<!-- To enable this, set global attribute`allow_relaxed_schema_actions=True`. This setting is used especially when a schema is useful for validation of arguments but not available - not for methods with no arguments.
283328
284329
```py title="Relaxed or Unavailable Schema for Actions" linenums="1"
285330
--8<-- "docs/beginners-guide/code/thing_example_2.py:168:172"
286331
--8<-- "docs/beginners-guide/code/thing_example_2.py:558:559"
287-
```
332+
``` -->
288333

289334
It is always possible to custom validate the arguments after invoking the action:
290335

@@ -293,7 +338,7 @@ It is always possible to custom validate the arguments after invoking the action
293338
--8<-- "docs/beginners-guide/code/actions/parameterized_function.py:9:27"
294339
```
295340

296-
The last and least preferred possibility is to use `ParameterizedFunction`:
341+
<!-- The last and least preferred possibility is to use `ParameterizedFunction`:
297342
298343
```py title="Parameterized Function" linenums="1"
299344
--8<-- "docs/beginners-guide/code/actions/parameterized_function.py:52:"
@@ -317,8 +362,89 @@ client side, there is no difference between invoking a normal action and an acti
317362
318363
```py title="Custom Validation" linenums="1"
319364
--8<-- "docs/beginners-guide/code/actions/parameterized_function.py:30:36"
320-
```
365+
``` -->
366+
367+
## Threaded & Async Actions
368+
369+
Actions can be made asynchronous or threaded by setting the `synchronous` flag to `False` in the decorator. For methods
370+
that are **not** `async`:
371+
372+
```py title="Threaded Actions" linenums="3"
373+
class ServoMotor(Thing):
374+
375+
@action(synchronous=False)
376+
def poll_device_state(self) -> str:
377+
"""check device state, especially when it got stuck up"""
378+
...
379+
380+
@action(threaded=True) # exactly the same effect for sync methods
381+
def poll_device_state(self) -> str:
382+
"""check device state, especially when it got stuck up"""
383+
...
384+
```
385+
386+
The return value is fetched and returned to the client. One could also start long running actions without fetching a return value,
387+
(although it would be better in many cases to manually thread out a long running action):
388+
389+
```py title="Threaded Actions" linenums="3"
390+
class DCPowerSupply(Thing):
391+
"""A DC Power Supply from 0-30V"""
392+
393+
@action(threaded=True)
394+
def monitor_over_voltage(self, period: float = 5):
395+
"""background voltage monitor loop"""
396+
while True:
397+
voltage = self.measure_voltage()
398+
if voltage > self.over_voltage_threshold:
399+
self.over_voltage_event(
400+
dict(
401+
timestamp=datetime.datetime.now().strftime(
402+
"%Y-%m-%d %H:%M:%S"
403+
),
404+
voltage=voltage
405+
)
406+
)
407+
time.sleep(period)
408+
# The suitability of this example in a realistic use case is untested
409+
```
410+
411+
Same applies for `async`:
412+
413+
```py title="Async Actions" linenums="3"
414+
class DCPowerSupply(Thing):
415+
416+
@action(create_task=True)
417+
async def monitor_over_voltage(self, period: float = 5):
418+
"""background monitor loop"""
419+
while True:
420+
voltage = await asyncio.get_running_loop().run_in_executor(
421+
None, self.measure_voltage
422+
)
423+
if voltage > self.over_voltage_threshold:
424+
self.over_voltage_event(
425+
dict(
426+
timestamp=datetime.datetime.now().strftime(
427+
"%Y-%m-%d %H:%M:%S"
428+
),
429+
voltage=voltage
430+
)
431+
)
432+
await asyncio.sleep(period)
433+
# The suitability of this example in a realistic use case is untested
434+
```
435+
436+
For long running actions that do not return, call them with `noblock` flag on the client, otherwise except a `TimeoutError`:
437+
438+
```py
439+
client.invoke_action("monitor_over_voltage", period=10, noblock=True)
440+
```
321441

322-
## Async & Threaded Actions
442+
## Thing Description Metadata
323443

324-
## TD
444+
| field | supported | description |
445+
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
446+
| input || schema of the input payload (validation carried out) |
447+
| output || schema of the output payload (validation not carried out) |
448+
| safe || whether the action is safe to execute, only treated as a metadata |
449+
| idempotent || whether the action is idempotent, `True` when the action is executable in all states of a state machine, otherwise `False` |
450+
| synchronous || whether the action is synchronous, `False` for threaded actions and async actions which are scheduled in the running event loop |

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ dependencies = [
1313
"mkdocs-material==9.6.14",
1414
"mkdocstrings[python]==0.29.1",
1515
"pydantic==2.11.5",
16+
"pip>=25.2",
1617
]
1718

1819
[tool.uv.sources]
19-
hololinked = { git = "https://github.com/hololinked-dev/hololinked.git", branch = "main" }
20+
hololinked = { git = "https://github.com/hololinked-dev/hololinked.git", branch = "main" }

uv.lock

Lines changed: 15 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)