Skip to content

Commit 814bbec

Browse files
author
Joel Collins
committed
Label actions as safe and/or idempotent
1 parent b73a19d commit 814bbec

File tree

4 files changed

+67
-3
lines changed

4 files changed

+67
-3
lines changed

examples/builder.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ def cleanup():
4444
)
4545
labthing.add_view(
4646
action_from(
47-
my_component.average_data, description="Take an averaged measurement", task=True
47+
my_component.average_data,
48+
description="Take an averaged measurement",
49+
task=True, # Is the action a long-running task?
50+
safe=True, # Is the state of the Thing changed by calling the action?
51+
idempotent=True, # Can the action be called repeatedly with the same result?
4852
),
4953
"/average",
5054
)

labthings/server/decorators.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def wrapper(*args, **kwargs):
9696

9797
if not isinstance(data, TaskThread):
9898
raise TypeError(
99-
f"Function {f.__name__} expected to return a TaskThread object, but instead returned a {type(data).__name__}. If it does not return a task, remove the @marshall_task decorator from {f.__name__}.",
99+
f"Function {f.__name__} expected to return a TaskThread object, but instead returned a {type(data).__name__}. If it does not return a task, remove the @marshall_task decorator from {f.__name__}."
100100
)
101101
return make_response(TaskSchema().jsonify(data), code, headers)
102102

@@ -121,6 +121,40 @@ def ThingAction(viewcls: View):
121121
thing_action = ThingAction
122122

123123

124+
def Safe(viewcls: View):
125+
"""Decorator to tag a view or function as being safe
126+
127+
Args:
128+
viewcls (View): View class to tag as Safe
129+
130+
Returns:
131+
View: View class with Safe spec tags
132+
"""
133+
# Update Views API spec
134+
update_spec(viewcls, {"_safe": True})
135+
return viewcls
136+
137+
138+
safe = Safe
139+
140+
141+
def Idempotent(viewcls: View):
142+
"""Decorator to tag a view or function as being idempotent
143+
144+
Args:
145+
viewcls (View): View class to tag as idempotent
146+
147+
Returns:
148+
View: View class with idempotent spec tags
149+
"""
150+
# Update Views API spec
151+
update_spec(viewcls, {"_idempotent": True})
152+
return viewcls
153+
154+
155+
idempotent = Idempotent
156+
157+
124158
def ThingProperty(viewcls):
125159
"""Decorator to tag a view as a Thing Property
126160

labthings/server/spec/td.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ def view_to_thing_property_forms(self, rules: list, view: View):
157157
def view_to_thing_action(self, rules: list, view: View):
158158
action_urls = [rule_to_path(rule) for rule in rules]
159159

160+
# Check if action is safe
161+
is_safe = get_spec(view.post).get("_safe", False) or get_spec(view).get(
162+
"_safe", False
163+
)
164+
165+
is_idempotent = get_spec(view.post).get("_idempotent", False) or get_spec(
166+
view
167+
).get("_idempotent", False)
168+
160169
# Basic description
161170
action_description = {
162171
"title": view.__name__,
@@ -166,6 +175,8 @@ def view_to_thing_action(self, rules: list, view: View):
166175
# TODO: Make URLs absolute
167176
"links": [{"href": f"{url}"} for url in action_urls],
168177
"forms": self.view_to_thing_action_forms(rules, view),
178+
"safe": is_safe,
179+
"idempotent": is_idempotent,
169180
}
170181

171182
# Look for a _propertySchema in the Property classes API SPec

labthings/server/view/builder.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
marshal_task,
1212
use_args,
1313
Doc,
14+
Safe,
15+
Idempotent,
1416
)
1517
from . import View
1618

@@ -74,7 +76,14 @@ def _put(self, args):
7476
return generated_class
7577

7678

77-
def action_from(function, name: str = None, description=None, task=False):
79+
def action_from(
80+
function,
81+
name: str = None,
82+
description=None,
83+
task=False,
84+
safe=False,
85+
idempotent=False,
86+
):
7887

7988
# Create a class name
8089
if not name:
@@ -108,6 +117,12 @@ def _post(self, args):
108117
generated_class
109118
)
110119

120+
if safe:
121+
generated_class = Safe(generated_class)
122+
123+
if idempotent:
124+
generated_class = Idempotent(generated_class)
125+
111126
return generated_class
112127

113128

0 commit comments

Comments
 (0)