Skip to content

Commit de513d5

Browse files
committed
re-read and optimize
1 parent 251477b commit de513d5

File tree

8 files changed

+127
-82
lines changed

8 files changed

+127
-82
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"editor.rulers": [
33
80,120
44
],
5-
"editor.formatOnSave": true,
5+
"editor.formatOnSave": true
66
}

docs/beginners-guide/articles/actions.md

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Only methods decorated with `action()` are exposed to clients.
66

7-
```py title="Actions" linenums="1"
7+
```py title="Actions" linenums="1" hl_lines="5 10 15 16 21 25"
88
--8<-- "docs/beginners-guide/code/thing_example_2.py:189:192"
99
--8<-- "docs/beginners-guide/code/thing_example_2.py:431:445"
1010
--8<-- "docs/beginners-guide/code/thing_example_2.py:643:646"
@@ -13,17 +13,16 @@ Only methods decorated with `action()` are exposed to clients.
1313

1414
## Payload Validation
1515

16-
Arguments are loosely typed and may need to be constrained with a schema based
17-
on the robustness the developer is expecting in their application:
16+
If arguments are loosely typed, the action will be invoked with given payload without any validation. One may validate them manually inside the method. However, one can also specify the expected argument schema using either `JSON Schema` or `pydantic` models:
1817

1918
<a id="actions-argument-schema"></a>
2019
=== "JSON Schema"
2120

2221
=== "Single Argument"
2322

24-
Just specify the expected type of the argument (with or without name)
23+
Specify the expected type of the argument (with or without name)
2524

26-
```py title="Input Schema" linenums="1"
25+
```py title="Input Schema" linenums="1" hl_lines="8"
2726
--8<-- "docs/beginners-guide/code/thing_example_2.py:189:192"
2827
--8<-- "docs/beginners-guide/code/thing_example_2.py:210:222"
2928
```
@@ -47,10 +46,10 @@ on the robustness the developer is expecting in their application:
4746

4847
=== "Multiple Arguments"
4948

50-
You need to specify the action argument names under the `properties` field with `type` as `object`.
51-
Names not found in the `properties` field can be subsumed under python spread operator `**kwargs` if necessary.
49+
Specify the argument names under the `properties` field with `type` as `object`.
50+
Names not found in the `properties` field can be subsumed under python spread operator `**kwargs` if necessary (dont set `additionalProperties` to `False` in that case).
5251

53-
```py title="Input Schema with Multiple Arguments" linenums="1"
52+
```py title="Input Schema with Multiple Arguments" linenums="1" hl_lines="32"
5453
--8<-- "docs/beginners-guide/code/thing_example_3.py:57:85"
5554
--8<-- "docs/beginners-guide/code/thing_example_3.py:87:87"
5655
--8<-- "docs/beginners-guide/code/thing_example_3.py:213:226"
@@ -99,7 +98,9 @@ on the robustness the developer is expecting in their application:
9998

10099
=== "Return Type"
101100

102-
```py title="With Return Type"
101+
Specify return type under `output_schema` field:
102+
103+
```py title="With Return Type" linenums="1" hl_lines="38 39"
103104
--8<-- "docs/beginners-guide/code/thing_example_3.py:22:55"
104105
--8<-- "docs/beginners-guide/code/thing_example_3.py:87:87"
105106
--8<-- "docs/beginners-guide/code/thing_example_3.py:349:356"
@@ -151,7 +152,9 @@ on the robustness the developer is expecting in their application:
151152

152153
=== "Single Argument"
153154

154-
```py title="Input Schema with Single Argument" linenums="1"
155+
Type annotate the argument, either plainly or with `Annotated`. A pydantic model will be composed with the argument name as the field name and the type annotation as the field type:
156+
157+
```py title="Input Schema with Single Argument" linenums="1" hl_lines="7"
155158
from typing import Annotated
156159

157160
class GentecOpticalEnergyMeter(Thing):
@@ -196,6 +199,8 @@ on the robustness the developer is expecting in their application:
196199

197200
=== "Multiple Arguments"
198201

202+
Again, type annotate the arguments, either plainly or with `Annotated`:
203+
199204
```py title="Input Schema with Multiple Arguments"
200205
from typing import Literal
201206

@@ -284,6 +289,8 @@ on the robustness the developer is expecting in their application:
284289

285290
=== "Return Type"
286291

292+
type annotate the return type:
293+
287294
```py
288295
from typing import Annotated
289296
from pydantic import Field
@@ -321,8 +328,38 @@ on the robustness the developer is expecting in their application:
321328
}
322329
```
323330

324-
However, a schema is optional and it only matters that
325-
the method signature is matching when requested from a client.
331+
=== "Supply Models Directly"
332+
333+
If the composed models from the type annotations are not sufficient or contain errors, one may directly supply the models:
334+
335+
```py title="With Direct Models" linenums="1" hl_lines="3 7 22"
336+
from pydantic import BaseModel, field_validator
337+
338+
class CommandModel(BaseModel):
339+
command: str
340+
return_data_size: int = Field(0, ge=0)
341+
342+
@field_validator("command")
343+
def validate_command(cls, v):
344+
if not isinstance(v, str) or not v:
345+
raise ValueError("Command must be a non-empty string")
346+
if command not in SerialUtility.supported_commands:
347+
raise ValueError(f"Command {command} is not supported")
348+
return v
349+
350+
class ResponseModel(BaseModel):
351+
response: str = Field(..., description="Response from the device")
352+
353+
class SerialUtility(Thing):
354+
355+
supported_commands = ["*IDN?", "MEAS:VOLT?", "MEAS:CURR?"]
356+
357+
@action(input_schema=CommandModel, output_schema=ResponseModel)
358+
def execute_instruction(self, command: str, return_data_size: int = 0) -> str:
359+
"""
360+
executes instruction given by the ASCII string parameter 'command'
361+
"""
362+
```
326363

327364
<!-- 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.
328365
@@ -366,8 +403,7 @@ client side, there is no difference between invoking a normal action and an acti
366403

367404
## Threaded & Async Actions
368405

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`:
406+
Actions can be made asynchronous or threaded by setting the `synchronous` flag to `False`. For methods that are **not** `async`:
371407

372408
```py title="Threaded Actions" linenums="3"
373409
class ServoMotor(Thing):
@@ -408,32 +444,32 @@ class DCPowerSupply(Thing):
408444
# The suitability of this example in a realistic use case is untested
409445
```
410446

411-
Same applies for `async`:
447+
For `async` actions:
412448

413449
```py title="Async Actions" linenums="3"
414450
class DCPowerSupply(Thing):
415451

416452
@action(create_task=True)
453+
# @action(synchronous=False) # exactly the same effect for async methods
417454
async def monitor_over_voltage(self, period: float = 5):
418455
"""background monitor loop"""
419456
while True:
420457
voltage = await asyncio.get_running_loop().run_in_executor(
421458
None, self.measure_voltage
422459
)
423460
if voltage > self.over_voltage_threshold:
461+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
424462
self.over_voltage_event(
425463
dict(
426-
timestamp=datetime.datetime.now().strftime(
427-
"%Y-%m-%d %H:%M:%S"
428-
),
464+
timestamp=timestamp,
429465
voltage=voltage
430466
)
431467
)
432468
await asyncio.sleep(period)
433469
# The suitability of this example in a realistic use case is untested
434470
```
435471

436-
For long running actions that do not return, call them with `oneway` flag on the client, otherwise except a `TimeoutError`:
472+
For long running actions that do not return, call them with `oneway` flag on the client, otherwise expect a `TimeoutError`:
437473

438474
```py
439475
client.invoke_action("monitor_over_voltage", period=10, oneway=True)

docs/beginners-guide/articles/events.md

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ One can subscribe to the event using the attribute name:
3030

3131
=== "async"
3232

33-
In the asynchronous mode, the `subscribe_event` method creates an event listening task in the running async loop:
33+
In the asynchronous mode, the `subscribe_event` method creates an event listening task in the running async loop. This requires the client to be running in an async loop, otherwise no events will be received although the server will be publishing it:
3434

3535
```py title="Subscription" linenums="1" hl_lines="9"
3636
from hololinked.client import ClientFactory
3737

3838
energy_meter = ClientFactory.http(url="http://localhost:8000/energy-meter")
3939
# energy_meter = ClientFactory.zmq(id="energy_meter", access_point="IPC")
4040

41-
def event_cb(event_data):
42-
print(event_data)
41+
def event_cb(event):
42+
print(event)
4343

4444
energy_meter.subscribe_event(
4545
name="data_point_event",
@@ -48,70 +48,76 @@ One can subscribe to the event using the attribute name:
4848
)
4949
```
5050

51-
---
51+
The callback function(s) must accept a single argument which is the event data payload, an instance of `SSE` object. The payload can be accessed as using the `data` attribute:
52+
53+
```py title="Event Data" linenums="1" hl_lines="9"
54+
def event_cb(event):
55+
print(event.data)
56+
```
5257

53-
One can also supply multiple callbacks which may called in series, threaded or async:
58+
> The `SSE` object also contains metadata like `id`, `event` name and `retry` interval, but these are currently not well supported. Improvements in the future are expected.
59+
60+
Each subscription creates a new event stream. One can also supply multiple callbacks which may called in series or concurrently:
5461

5562
=== "sequential"
5663

5764
The background thread that listens to the event executes the callbacks in series in its own thread:
5865

5966
```py title="Sequential Callbacks" linenums="1" hl_lines="9"
60-
def event_cb1(event_data):
61-
print("First Callback", event_data)
67+
def event_cb1(event):
68+
print("First Callback", event.data)
6269

63-
def event_cb2(event_data):
64-
print("Second callback", event_data)
70+
def event_cb2(event):
71+
print("Second callback", event.data)
6572

6673
energy_meter.subscribe_event(
6774
name="statistics_event",
6875
callbacks=[event_cb1, event_cb2]
76+
# This also works for async where all callbacks are awaited in series
6977
)
7078
```
7179

72-
So please be careful while using GUI frameworks like PyQt where you can paint the GUI only from the main thread.
73-
You would need to use signals and slots or other mechanisms.
74-
7580
=== "threaded"
7681

7782
The background thread that listens to the event executes the callbacks by spawning new threads:
7883

7984
```py title="Thread Callbacks" linenums="1" hl_lines="9-10"
80-
def event_cb1(event_data):
81-
print("First Callback", event_data)
85+
def event_cb1(event):
86+
print("First Callback", event.data)
8287

83-
def event_cb2(event_data):
84-
print("Second callback", event_data)
88+
def event_cb2(event):
89+
print("Second callback", event.data)
8590

8691
energy_meter.subscribe_event(
8792
name="statistics_event",
8893
callbacks=[event_cb1, event_cb2],
89-
thread_callbacks=True
94+
concurrent=True
9095
)
9196
```
92-
Again, please be careful while using GUI frameworks like PyQt where you can paint the GUI only from the main thread.
9397

9498
=== "async"
9599

96-
Applies only when listening to event with `async=True`, the `async` method creates new tasks in the current loop:
100+
The event listening task creates newer tasks in the running event loop:
97101

98-
```py title="Thread Callbacks" linenums="1"
99-
async def event_cb1(event_data):
100-
print("First Callback", event_data)
101-
await some_async_function1(event_data)
102+
```py title="Thread Callbacks" linenums="1" hl_lines="12-13"
103+
async def event_cb1(event):
104+
print("First Callback", event.data)
105+
await some_async_function1(event.data)
102106

103-
async def event_cb2(event_data):
104-
print("Second callback", event_data)
105-
await some_async_function2(event_data)
107+
async def event_cb2(event):
108+
print("Second callback", event.data)
109+
await some_async_function2(event.data)
106110

107111
energy_meter.subscribe_event(
108112
name="statistics_event",
109113
callbacks=[event_cb1, event_cb2],
110114
asynch=True,
111-
create_task_for_cbs=True
115+
concurrent=True
112116
)
113117
```
114118

119+
> In GUI frameworks like PyQt, you cannot paint the GUI from the event thread. You would need to use signals and slots or other mechanisms to update the GUI to hand over the data.
120+
115121
---
116122

117123
To unsubscribe:
@@ -120,11 +126,13 @@ To unsubscribe:
120126
energy_meter.unsubscribe_event(name="data_point_event")
121127
```
122128

129+
All subscriptions to the same event are removed.
130+
123131
## Payload Schema
124132

125-
Schema may be supplied for the validation of the event data on the client:
133+
Schema may be supplied for the validation of the event data on the client using pydantic or JSON schema:
126134

127-
```py title="" linenums="1" hl_lines="13"
135+
```py title="Payload Schema" linenums="1" hl_lines="13"
128136
class GentecMaestroEnergyMeter(Thing):
129137

130138
data_point_event_schema = {
@@ -145,6 +153,8 @@ class GentecMaestroEnergyMeter(Thing):
145153

146154
There is no separate validation on the server side.
147155

156+
> There is no validation on the client side currently implemented in `hololinked.client`. This will be added in future releases.
157+
148158
???+ "Schema as seen in Thing Description"
149159

150160
```py
@@ -172,9 +182,9 @@ There is no separate validation on the server side.
172182

173183
## Thing Description Metadata
174184

175-
| Key | Supported | Comment |
176-
| ------------ | --------- | ---------------------------------------------------------------------------------- |
177-
| subscription || |
178-
| data || payload schema for the event |
179-
| dataResponse || schema for response message after arrival of an event, will be supported in future |
180-
| cancellation | - | Server sent events can be cancelled by the client directly |
185+
| Key | Supported | Comment |
186+
| ------------ | --------- | ---------------------------------------------------------- |
187+
| subscription || |
188+
| data || payload schema for the event |
189+
| dataResponse || will be supported in a future release |
190+
| cancellation | - | Server sent events can be cancelled by the client directly |

0 commit comments

Comments
 (0)