|
1 | | -# Python LabThings (for Flask) |
2 | | - |
3 | | -[](https://github.com/labthings/) |
4 | | -[](https://python-labthings.readthedocs.io/en/latest/) |
5 | | -[](https://pypi.org/project/labthings/) |
6 | | -[](https://github.com/psf/black) |
7 | | -[](https://codecov.io/gh/labthings/python-labthings) |
8 | | -[](https://riot.im/app/#/room/#labthings:matrix.org) |
9 | | - |
10 | | -A thread-based Python implementation of the LabThings API structure, based on the Flask microframework. |
11 | | - |
12 | | -## Installation |
13 | | - |
14 | | -`pip install labthings` |
15 | | - |
16 | | -## Quickstart example |
17 | | - |
18 | | -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. |
19 | | - |
20 | | -```python |
21 | | -#!/usr/bin/env python |
22 | | -import time |
23 | | - |
24 | | -from labthings import ActionView, PropertyView, create_app, fields, find_component, op |
25 | | -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) |
129 | | - |
130 | | - |
131 | | -# Create LabThings Flask app |
132 | | -app, labthing = create_app( |
133 | | - __name__, |
134 | | - title="My Lab Device API", |
135 | | - description="Test LabThing-based API", |
136 | | - version="0.1.0", |
137 | | -) |
138 | | - |
139 | | -# Attach an instance of our component |
140 | | -# Usually a Python object controlling some piece of hardware |
141 | | -my_spectrometer = PretendSpectrometer() |
142 | | -labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") |
143 | | - |
144 | | - |
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") |
149 | | - |
150 | | - |
151 | | -# Start the app |
152 | | -if __name__ == "__main__": |
153 | | - from labthings import Server |
154 | | - |
155 | | - Server(app).run() |
156 | | -``` |
157 | | - |
158 | | -## Acknowledgements |
159 | | - |
160 | | -Much of the code surrounding default response formatting has been liberally taken from [Flask-RESTful](https://github.com/flask-restful/flask-restful). The integrated [Marshmallow](https://github.com/marshmallow-code/marshmallow) support was inspired by [Flask-Marshmallow](https://github.com/marshmallow-code/flask-marshmallow) and [Flask-ApiSpec](https://github.com/jmcarp/flask-apispec). |
161 | | - |
162 | | -## Developer notes |
163 | | - |
164 | | -### Changelog generation |
165 | | - |
166 | | -* `npm install -g conventional-changelog-cli` |
167 | | -* `conventional-changelog -r 1 --config ./changelog.config.js -i CHANGELOG.md -s` |
| 1 | +# Python LabThings (for Flask) |
| 2 | + |
| 3 | +[](https://github.com/labthings/) |
| 4 | +[](https://python-labthings.readthedocs.io/en/latest/) |
| 5 | +[](https://pypi.org/project/labthings/) |
| 6 | +[](https://github.com/psf/black) |
| 7 | +[](https://codecov.io/gh/labthings/python-labthings) |
| 8 | +[](https://riot.im/app/#/room/#labthings:matrix.org) |
| 9 | + |
| 10 | +A thread-based Python implementation of the LabThings API structure, based on the Flask microframework. |
| 11 | + |
| 12 | +## Installation |
| 13 | + |
| 14 | +`pip install labthings` |
| 15 | + |
| 16 | +## Quickstart example |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +```python |
| 21 | +#!/usr/bin/env python |
| 22 | +import time |
| 23 | + |
| 24 | +from labthings import ActionView, PropertyView, create_app, fields, find_component, op |
| 25 | +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 | + |
| 64 | +""" |
| 65 | +Create a view to quickly get some noisy data, and register is as a Thing property |
| 66 | +""" |
| 67 | + |
| 68 | + |
| 69 | +class QuickDataProperty(PropertyView): |
| 70 | + """Show the current data value""" |
| 71 | + |
| 72 | + # Marshal the response as a list of floats |
| 73 | + schema = fields.List(fields.Float()) |
| 74 | + |
| 75 | + @op.readproperty |
| 76 | + def get(self): |
| 77 | + # Find our attached component |
| 78 | + my_component = find_component("org.labthings.example.mycomponent") |
| 79 | + return my_component.data |
| 80 | + |
| 81 | + |
| 82 | + |
| 83 | +""" |
| 84 | +Create a view to start an averaged measurement, and register is as a Thing action |
| 85 | +""" |
| 86 | + |
| 87 | + |
| 88 | +class MeasurementAction(ActionView): |
| 89 | + # Expect JSON parameters in the request body. |
| 90 | + # Pass to post function as dictionary argument. |
| 91 | + args = { |
| 92 | + "averages": fields.Integer( |
| 93 | + missing=20, example=20, description="Number of data sets to average over", |
| 94 | + ) |
| 95 | + } |
| 96 | + # Marshal the response as a list of numbers |
| 97 | + schema = fields.List(fields.Number) |
| 98 | + |
| 99 | + # Main function to handle POST requests |
| 100 | + @op.invokeaction |
| 101 | + def post(self, args): |
| 102 | + """Start an averaged measurement""" |
| 103 | + |
| 104 | + # Find our attached component |
| 105 | + my_component = find_component("org.labthings.example.mycomponent") |
| 106 | + |
| 107 | + # Get arguments and start a background task |
| 108 | + n_averages = args.get("averages") |
| 109 | + |
| 110 | + # Return the task information |
| 111 | + return my_component.average_data(n_averages) |
| 112 | + |
| 113 | + |
| 114 | +# Create LabThings Flask app |
| 115 | +app, labthing = create_app( |
| 116 | + __name__, |
| 117 | + title="My Lab Device API", |
| 118 | + description="Test LabThing-based API", |
| 119 | + version="0.1.0", |
| 120 | +) |
| 121 | + |
| 122 | +# Attach an instance of our component |
| 123 | +# Usually a Python object controlling some piece of hardware |
| 124 | +my_spectrometer = PretendSpectrometer() |
| 125 | +labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") |
| 126 | + |
| 127 | + |
| 128 | +# Add routes for the API views we created |
| 129 | +labthing.add_view(DenoiseProperty, "/integration_time") |
| 130 | +labthing.add_view(QuickDataProperty, "/quick-data") |
| 131 | +labthing.add_view(MeasurementAction, "/actions/measure") |
| 132 | + |
| 133 | + |
| 134 | +# Start the app |
| 135 | +if __name__ == "__main__": |
| 136 | + from labthings import Server |
| 137 | + |
| 138 | + Server(app).run() |
| 139 | +``` |
| 140 | + |
| 141 | +## Acknowledgements |
| 142 | + |
| 143 | +Much of the code surrounding default response formatting has been liberally taken from [Flask-RESTful](https://github.com/flask-restful/flask-restful). The integrated [Marshmallow](https://github.com/marshmallow-code/marshmallow) support was inspired by [Flask-Marshmallow](https://github.com/marshmallow-code/flask-marshmallow) and [Flask-ApiSpec](https://github.com/jmcarp/flask-apispec). |
| 144 | + |
| 145 | +## Developer notes |
| 146 | + |
| 147 | +### Changelog generation |
| 148 | + |
| 149 | +* `npm install -g conventional-changelog-cli` |
| 150 | +* `conventional-changelog -r 1 --config ./changelog.config.js -i CHANGELOG.md -s` |
0 commit comments