Skip to content

Commit 12b6dda

Browse files
author
Joel Collins
committed
Introduced simple extension lifecycle functions
1 parent f3167ef commit 12b6dda

File tree

3 files changed

+209
-0
lines changed

3 files changed

+209
-0
lines changed

examples/simple_extensions.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import random
2+
import math
3+
import time
4+
import logging
5+
6+
from labthings.server.quick import create_app
7+
from labthings.server.decorators import (
8+
ThingAction,
9+
ThingProperty,
10+
PropertySchema,
11+
use_args,
12+
use_body,
13+
marshal_task,
14+
marshal_with,
15+
)
16+
from labthings.server.view import View
17+
from labthings.server.find import find_component
18+
from labthings.server import fields
19+
from labthings.core.tasks import taskify
20+
21+
from labthings.server.extensions import BaseExtension
22+
23+
import logging
24+
25+
logging.basicConfig(level=logging.DEBUG)
26+
27+
"""
28+
Make our extension
29+
"""
30+
31+
32+
def ext_on_register():
33+
logging.info("Extension registered")
34+
35+
36+
def ext_on_my_component(component):
37+
logging.info(f"{component} registered and noticed by extension")
38+
39+
40+
example_extension = BaseExtension("org.labthings.examples.extension")
41+
example_extension.on_register(ext_on_register)
42+
example_extension.on_component("org.labthings.example.mycomponent", ext_on_my_component)
43+
44+
45+
"""
46+
Class for our lab component functionality. This could include serial communication,
47+
equipment API calls, network requests, or a "virtual" device as seen here.
48+
"""
49+
50+
51+
class MyComponent:
52+
def __init__(self):
53+
self.x_range = range(-100, 100)
54+
self.magic_denoise = 200
55+
56+
def noisy_pdf(self, x, mu=0.0, sigma=25.0):
57+
"""
58+
Generate a noisy gaussian function (to act as some pretend data)
59+
60+
Our noise is inversely proportional to self.magic_denoise
61+
"""
62+
x = float(x - mu) / sigma
63+
return (
64+
math.exp(-x * x / 2.0) / math.sqrt(2.0 * math.pi) / sigma
65+
+ (1 / self.magic_denoise) * random.random()
66+
)
67+
68+
@property
69+
def data(self):
70+
"""
71+
Return a 1D data trace.
72+
"""
73+
return [self.noisy_pdf(x) for x in self.x_range]
74+
75+
76+
"""
77+
Create a view to view and change our magic_denoise value, and register is as a Thing property
78+
"""
79+
80+
81+
@ThingProperty # Register this view as a Thing Property
82+
@PropertySchema( # Define the data we're going to output (get), and what to expect in (post)
83+
fields.Integer(
84+
required=True,
85+
example=200,
86+
minimum=100,
87+
maximum=500,
88+
description="Value of magic_denoise",
89+
)
90+
)
91+
class DenoiseProperty(View):
92+
93+
# Main function to handle GET requests (read)
94+
def get(self):
95+
"""Show the current magic_denoise value"""
96+
97+
# When a GET request is made, we'll find our attached component
98+
my_component = find_component("org.labthings.example.mycomponent")
99+
return my_component.magic_denoise
100+
101+
# Main function to handle POST requests (write)
102+
def post(self, new_property_value):
103+
"""Change the current magic_denoise value"""
104+
105+
# Find our attached component
106+
my_component = find_component("org.labthings.example.mycomponent")
107+
108+
# Apply the new value
109+
my_component.magic_denoise = new_property_value
110+
111+
return my_component.magic_denoise
112+
113+
114+
"""
115+
Create a view to quickly get some noisy data, and register is as a Thing property
116+
"""
117+
118+
119+
@ThingProperty
120+
@PropertySchema(fields.List(fields.Float()))
121+
class QuickDataProperty(View):
122+
# Main function to handle GET requests
123+
def get(self):
124+
"""Show the current data value"""
125+
126+
# Find our attached component
127+
my_component = find_component("org.labthings.example.mycomponent")
128+
return my_component.data
129+
130+
131+
# Create LabThings Flask app
132+
app, labthing = create_app(
133+
__name__,
134+
prefix="/api",
135+
title=f"My Lab Device API",
136+
description="Test LabThing-based API",
137+
version="0.1.0",
138+
)
139+
140+
# Register extensions
141+
labthing.register_extension(example_extension)
142+
143+
# Attach an instance of our component
144+
labthing.add_component(MyComponent(), "org.labthings.example.mycomponent")
145+
146+
# Add routes for the API views we created
147+
labthing.add_view(DenoiseProperty, "/denoise")
148+
labthing.add_view(QuickDataProperty, "/quick-data")
149+
150+
151+
# Start the app
152+
if __name__ == "__main__":
153+
app.run(host="0.0.0.0", port="5000", threaded=True, debug=True, use_reloader=False)

labthings/server/extensions.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ def __init__(self, name: str, description="", version="0.0.0"):
2525
self._rules = {} # Key: Original rule. Val: View class
2626
self._meta = {} # Extra metadata to add to the extension description
2727

28+
self._on_registers = (
29+
[]
30+
) # List of dictionaries of functions to run on registration
31+
32+
self._on_components = (
33+
[]
34+
) # List of dictionaries of functions to run as components are added
35+
2836
self._cls = str(self) # String description of extension instance
2937

3038
self.actions = []
@@ -59,6 +67,27 @@ def add_view(self, view_class, rule, **kwargs):
5967
# Store the rule expansion information
6068
self._rules[rule] = self._views[view_id]
6169

70+
def on_register(self, function, args=None, kwargs=None):
71+
if not callable(function):
72+
raise TypeError("Function must be a callable")
73+
74+
self._on_registers.append(
75+
{"function": function, "args": args or (), "kwargs": kwargs or {}}
76+
)
77+
78+
def on_component(self, component_name: str, function, args=None, kwargs=None):
79+
if not callable(function):
80+
raise TypeError("Function must be a callable")
81+
82+
self._on_components.append(
83+
{
84+
"component": component_name,
85+
"function": function,
86+
"args": args or (),
87+
"kwargs": kwargs or {},
88+
}
89+
)
90+
6291
@property
6392
def meta(self):
6493
d = {}

labthings/server/labthing.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ def _create_base_routes(self):
125125
def add_component(self, device_object, device_name: str):
126126
self.components[device_name] = device_object
127127

128+
for extension_object in self.extensions.values():
129+
# For each on_component function
130+
for com_func in extension_object._on_components:
131+
# If the component matches
132+
if com_func.get("component", "") == device_name:
133+
# Call the function
134+
com_func.get("function")(
135+
device_object, *com_func.get("args"), **com_func.get("kwargs")
136+
)
137+
128138
# Extension stuff
129139

130140
def register_extension(self, extension_object):
@@ -141,6 +151,23 @@ def register_extension(self, extension_object):
141151
**extension_view["kwargs"],
142152
)
143153

154+
# For each on_register function
155+
for reg_func in extension_object._on_registers:
156+
# Call the function
157+
reg_func.get("function")(*reg_func.get("args"), **reg_func.get("kwargs"))
158+
159+
# For each on_component function
160+
for com_func in extension_object._on_components:
161+
key = com_func.get("component", "")
162+
# If the component has already been added
163+
if key in self.components:
164+
# Call the function
165+
com_func.get("function")(
166+
self.components.get(key),
167+
*com_func.get("args"),
168+
**com_func.get("kwargs"),
169+
)
170+
144171
# Resource stuff
145172

146173
def _complete_url(self, url_part, registration_prefix):

0 commit comments

Comments
 (0)