Skip to content

Commit ae86f50

Browse files
authored
Merge pull request #139 from labthings/1.0-reset
WIP: Remove deprecated code and simplify structure
2 parents 4fc2def + 2493c9a commit ae86f50

116 files changed

Lines changed: 854 additions & 2089 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[flake8]
2-
max-line-length = 88
2+
ignore=E501
33
exclude = tests/*

README.md

Lines changed: 119 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![codecov](https://codecov.io/gh/labthings/python-labthings/branch/master/graph/badge.svg)](https://codecov.io/gh/labthings/python-labthings)
88
[![Riot.im](https://img.shields.io/badge/chat-on%20riot.im-368BD6)](https://riot.im/app/#/room/#labthings:matrix.org)
99

10-
A Python implementation of the LabThings API structure, based on the Flask microframework.
10+
A thread-based Python implementation of the LabThings API structure, based on the Flask microframework.
1111

1212
## Installation
1313

@@ -18,54 +18,140 @@ A Python implementation of the LabThings API structure, based on the Flask micro
1818
This example assumes a `PretendSpectrometer` class, which already has `data` and `integration_time` attributes, as well as an `average_data(n)` method. LabThings allows you to easily convert this existing instrument control code into a fully documented, standardised web API complete with auto-discovery and automatic background task threading.
1919

2020
```python
21-
from labthings import fields, create_app
21+
#!/usr/bin/env python
22+
import time
23+
24+
from labthings import ActionView, PropertyView, create_app, fields, find_component, op
2225
from labthings.example_components import PretendSpectrometer
26+
from labthings.json import encode_json
27+
28+
"""
29+
Class for our lab component functionality. This could include serial communication,
30+
equipment API calls, network requests, or a "virtual" device as seen here.
31+
"""
32+
33+
34+
"""
35+
Create a view to view and change our integration_time value,
36+
and register is as a Thing property
37+
"""
38+
39+
40+
# Wrap in a semantic annotation to autmatically set schema and args
41+
class DenoiseProperty(PropertyView):
42+
"""Value of integration_time"""
43+
44+
schema = fields.Int(required=True, minimum=100, maximum=500)
45+
semtype = "LevelProperty"
46+
47+
@op.readproperty
48+
def get(self):
49+
# When a GET request is made, we'll find our attached component
50+
my_component = find_component("org.labthings.example.mycomponent")
51+
return my_component.integration_time
52+
53+
@op.writeproperty
54+
def put(self, new_property_value):
55+
# Find our attached component
56+
my_component = find_component("org.labthings.example.mycomponent")
57+
58+
# Apply the new value
59+
my_component.integration_time = new_property_value
60+
61+
return my_component.integration_time
62+
63+
@op.observeproperty
64+
def websocket(self, ws):
65+
# Find our attached component
66+
my_component = find_component("org.labthings.example.mycomponent")
67+
initial_value = None
68+
while not ws.closed:
69+
time.sleep(1)
70+
if my_component.integration_time != initial_value:
71+
ws.send(encode_json(my_component.integration_time))
72+
initial_value = my_component.integration_time
73+
74+
75+
"""
76+
Create a view to quickly get some noisy data, and register is as a Thing property
77+
"""
78+
79+
80+
class QuickDataProperty(PropertyView):
81+
"""Show the current data value"""
82+
83+
# Marshal the response as a list of floats
84+
schema = fields.List(fields.Float())
85+
86+
@op.readproperty
87+
def get(self):
88+
# Find our attached component
89+
my_component = find_component("org.labthings.example.mycomponent")
90+
return my_component.data
91+
92+
@op.observeproperty
93+
def websocket(self, ws):
94+
# Find our attached component
95+
my_component = find_component("org.labthings.example.mycomponent")
96+
while not ws.closed:
97+
ws.send(encode_json(my_component.data))
98+
99+
100+
"""
101+
Create a view to start an averaged measurement, and register is as a Thing action
102+
"""
103+
104+
105+
class MeasurementAction(ActionView):
106+
# Expect JSON parameters in the request body.
107+
# Pass to post function as dictionary argument.
108+
args = {
109+
"averages": fields.Integer(
110+
missing=20, example=20, description="Number of data sets to average over",
111+
)
112+
}
113+
# Marshal the response as a list of numbers
114+
schema = fields.List(fields.Number)
115+
116+
# Main function to handle POST requests
117+
@op.invokeaction
118+
def post(self, args):
119+
"""Start an averaged measurement"""
120+
121+
# Find our attached component
122+
my_component = find_component("org.labthings.example.mycomponent")
123+
124+
# Get arguments and start a background task
125+
n_averages = args.get("averages")
126+
127+
# Return the task information
128+
return my_component.average_data(n_averages)
23129

24130

25131
# Create LabThings Flask app
26132
app, labthing = create_app(
27133
__name__,
28-
title="My PretendSpectrometer API",
29-
description="LabThing API for PretendSpectrometer",
30-
version="0.1.0"
134+
title="My Lab Device API",
135+
description="Test LabThing-based API",
136+
version="0.1.0",
31137
)
32138

33-
34-
# Make some properties and actions out of our component
139+
# Attach an instance of our component
140+
# Usually a Python object controlling some piece of hardware
35141
my_spectrometer = PretendSpectrometer()
142+
labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent")
36143

37-
# Single-shot data property
38-
labthing.build_property(
39-
my_spectrometer, # Python object
40-
"data", # Objects attribute name
41-
description="A single-shot measurement",
42-
readonly=True,
43-
schema=fields.List(fields.Number())
44-
)
45-
46-
# Integration time property
47-
labthing.build_property(
48-
my_spectrometer, # Python object
49-
"integration_time", # Objects attribute name
50-
description="Single-shot integration time",
51-
schema=fields.Int(min=100, max=500, example=200, unit="microsecond")
52-
)
53144

54-
# Averaged measurement action
55-
labthing.build_action(
56-
my_spectrometer, # Python object
57-
"average_data", # Objects method name
58-
description="Take an averaged measurement",
59-
schema=fields.List(fields.Number()),
60-
args={ # How do we convert from the request input to function arguments?
61-
"n": fields.Int(description="Number of averages to take", example=5, default=5)
62-
},
63-
)
145+
# Add routes for the API views we created
146+
labthing.add_view(DenoiseProperty, "/integration_time")
147+
labthing.add_view(QuickDataProperty, "/quick-data")
148+
labthing.add_view(MeasurementAction, "/actions/measure")
64149

65150

66151
# Start the app
67152
if __name__ == "__main__":
68153
from labthings import Server
154+
69155
Server(app).run()
70156
```
71157

docs/advanced_usage/view_class.rst

Lines changed: 0 additions & 13 deletions
This file was deleted.

docs/basic_usage/app_thing_server.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,16 @@ The LabThing object is our main entrypoint, and handles creating API views, mana
3333
.. autoclass:: labthings.LabThing
3434
:noindex:
3535

36-
Two key methods are :meth:`labthings.LabThing.build_property` and :meth:`labthings.LabThing.build_action`. These methods allow the automation creation of Property and Action API views from Python object attributes and methods. By passing schemas to these methods, argument and response marshalling is automatically performed. Offloading actions to background threads is also handled automatically.
3736

38-
.. automethod:: labthings.LabThing.build_property
37+
Views
38+
-----
39+
40+
Thing interaction affordances are created using Views. Two main View types correspond to properties and actions.
41+
42+
.. autoclass:: labthings.PropertyView
3943
:noindex:
4044

41-
.. automethod:: labthings.LabThing.build_action
45+
.. autoclass:: labthings.ActionView
4246
:noindex:
4347

4448

docs/quickstart.rst

Lines changed: 117 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,54 +14,139 @@ An example Lab Thing built from our ``PretendSpectrometer`` class, complete with
1414

1515
.. code-block:: python
1616
17-
from labthings import fields, create_app
17+
import time
18+
19+
from labthings import ActionView, PropertyView, create_app, fields, find_component, op
1820
from labthings.example_components import PretendSpectrometer
21+
from labthings.json import encode_json
22+
23+
"""
24+
Class for our lab component functionality. This could include serial communication,
25+
equipment API calls, network requests, or a "virtual" device as seen here.
26+
"""
27+
28+
29+
"""
30+
Create a view to view and change our integration_time value,
31+
and register is as a Thing property
32+
"""
33+
34+
35+
# Wrap in a semantic annotation to autmatically set schema and args
36+
class DenoiseProperty(PropertyView):
37+
"""Value of integration_time"""
38+
39+
schema = fields.Int(required=True, minimum=100, maximum=500)
40+
semtype = "LevelProperty"
41+
42+
@op.readproperty
43+
def get(self):
44+
# When a GET request is made, we'll find our attached component
45+
my_component = find_component("org.labthings.example.mycomponent")
46+
return my_component.integration_time
47+
48+
@op.writeproperty
49+
def put(self, new_property_value):
50+
# Find our attached component
51+
my_component = find_component("org.labthings.example.mycomponent")
52+
53+
# Apply the new value
54+
my_component.integration_time = new_property_value
55+
56+
return my_component.integration_time
57+
58+
@op.observeproperty
59+
def websocket(self, ws):
60+
# Find our attached component
61+
my_component = find_component("org.labthings.example.mycomponent")
62+
initial_value = None
63+
while not ws.closed:
64+
time.sleep(1)
65+
if my_component.integration_time != initial_value:
66+
ws.send(encode_json(my_component.integration_time))
67+
initial_value = my_component.integration_time
68+
69+
70+
"""
71+
Create a view to quickly get some noisy data, and register is as a Thing property
72+
"""
73+
74+
75+
class QuickDataProperty(PropertyView):
76+
"""Show the current data value"""
77+
78+
# Marshal the response as a list of floats
79+
schema = fields.List(fields.Float())
80+
81+
@op.readproperty
82+
def get(self):
83+
# Find our attached component
84+
my_component = find_component("org.labthings.example.mycomponent")
85+
return my_component.data
86+
87+
@op.observeproperty
88+
def websocket(self, ws):
89+
# Find our attached component
90+
my_component = find_component("org.labthings.example.mycomponent")
91+
while not ws.closed:
92+
ws.send(encode_json(my_component.data))
93+
94+
95+
"""
96+
Create a view to start an averaged measurement, and register is as a Thing action
97+
"""
98+
99+
100+
class MeasurementAction(ActionView):
101+
# Expect JSON parameters in the request body.
102+
# Pass to post function as dictionary argument.
103+
args = {
104+
"averages": fields.Integer(
105+
missing=20, example=20, description="Number of data sets to average over",
106+
)
107+
}
108+
# Marshal the response as a list of numbers
109+
schema = fields.List(fields.Number)
110+
111+
# Main function to handle POST requests
112+
@op.invokeaction
113+
def post(self, args):
114+
"""Start an averaged measurement"""
115+
116+
# Find our attached component
117+
my_component = find_component("org.labthings.example.mycomponent")
118+
119+
# Get arguments and start a background task
120+
n_averages = args.get("averages")
121+
122+
# Return the task information
123+
return my_component.average_data(n_averages)
19124
20125
21126
# Create LabThings Flask app
22127
app, labthing = create_app(
23128
__name__,
24-
title="My PretendSpectrometer API",
25-
description="LabThing API for PretendSpectrometer",
26-
version="0.1.0"
129+
title="My Lab Device API",
130+
description="Test LabThing-based API",
131+
version="0.1.0",
27132
)
28133
29-
30-
# Make some properties and actions out of our component
134+
# Attach an instance of our component
135+
# Usually a Python object controlling some piece of hardware
31136
my_spectrometer = PretendSpectrometer()
137+
labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent")
32138
33-
# Single-shot data property
34-
labthing.build_property(
35-
my_spectrometer, # Python object
36-
"data", # Objects attribute name
37-
description="A single-shot measurement",
38-
readonly=True,
39-
schema=fields.List(fields.Number())
40-
)
41139
42-
# Integration time property
43-
labthing.build_property(
44-
my_spectrometer, # Python object
45-
"integration_time", # Objects attribute name
46-
description="Single-shot integration time",
47-
schema=fields.Int(min=100, max=500, example=200, unit="microsecond")
48-
)
49-
50-
# Averaged measurement action
51-
labthing.build_action(
52-
my_spectrometer, # Python object
53-
"average_data", # Objects method name
54-
description="Take an averaged measurement",
55-
schema=fields.List(fields.Number()),
56-
args={ # How do we convert from the request input to function arguments?
57-
"n": fields.Int(description="Number of averages to take", example=5, default=5)
58-
},
59-
)
140+
# Add routes for the API views we created
141+
labthing.add_view(DenoiseProperty, "/integration_time")
142+
labthing.add_view(QuickDataProperty, "/quick-data")
143+
labthing.add_view(MeasurementAction, "/actions/measure")
60144
61145
62146
# Start the app
63147
if __name__ == "__main__":
64148
from labthings import Server
149+
65150
Server(app).run()
66151
67152

0 commit comments

Comments
 (0)