1
- from flask .views import MethodView
1
+ from flask .views import MethodView , http_method_funcs
2
2
from flask import request
3
3
from werkzeug .wrappers import Response as ResponseBase
4
4
from werkzeug .exceptions import BadRequest
8
8
from .args import use_args
9
9
from .marshalling import marshal_with
10
10
11
- from ..utilities import unpack
11
+ from ..utilities import unpack , get_docstring , get_summary
12
12
from ..representations import DEFAULT_REPRESENTATIONS
13
13
from ..find import current_labthing
14
14
from ..event import PropertyStatusEvent
15
15
from ..schema import Schema , ActionSchema , build_action_schema
16
16
from ..tasks import taskify
17
+ from ..deque import Deque , resize_deque
17
18
18
19
from gevent .timeout import Timeout
19
20
@@ -28,31 +29,15 @@ class View(MethodView):
28
29
get(), put(), post(), and delete(), corresponding to HTTP methods.
29
30
30
31
These functions will allow for automated documentation generation.
31
-
32
- Unlike MethodView, a LabThings View is opinionated, in that unless
33
- explicitally returning a Response object, all requests with be marshaled
34
- with the same schema, and all request arguments will be parsed with the same
35
- args schema
36
32
"""
37
33
38
34
endpoint = None
39
-
40
- schema : Schema = None
41
- args : dict = None
42
35
semtype : str = None
36
+
43
37
tags : list = [] # Custom tags the user can add
44
38
_cls_tags = set () # Class tags that shouldn't be removed
45
39
title : None
46
40
47
- # Default input content_type
48
- content_type = "application/json"
49
- # Custom responses dictionary
50
- responses : dict = {}
51
- # Methods for which to read arguments
52
- arg_methods = ("POST" , "PUT" , "PATCH" )
53
- # Methods for which to marshal responses
54
- marshal_methods = ("GET" , "PUT" , "POST" , "PATCH" )
55
-
56
41
def __init__ (self , * args , ** kwargs ):
57
42
MethodView .__init__ (self , * args , ** kwargs )
58
43
@@ -61,18 +46,23 @@ def __init__(self, *args, **kwargs):
61
46
self .representations = OrderedDict (DEFAULT_REPRESENTATIONS )
62
47
63
48
@classmethod
64
- def get_responses (cls ):
65
- r = {200 : {"schema" : cls .schema , "content_type" : "application/json" ,}}
66
- r .update (cls .responses )
67
- return r
68
-
69
- @classmethod
70
- def get_schema (cls ):
71
- return cls .schema
72
-
73
- @classmethod
74
- def get_args (cls ):
75
- return cls .args
49
+ def get_apispec (cls ):
50
+ d = {}
51
+
52
+ for method in http_method_funcs :
53
+ if hasattr (cls , method ):
54
+ d [method ] = {
55
+ "description" : getattr (cls , "description" , None )
56
+ or get_docstring (cls ),
57
+ "summary" : getattr (cls , "summary" , None ) or get_summary (cls ),
58
+ "tags" : list (cls .get_tags ()),
59
+ }
60
+
61
+ # Enable custom responses from all methods
62
+ if getattr (cls , "responses" , None ):
63
+ for method in d .keys ():
64
+ d [method ]["responses" ] = getattr (cls , "responses" )
65
+ return d
76
66
77
67
@classmethod
78
68
def get_tags (cls ):
@@ -98,14 +88,6 @@ def dispatch_request(self, *args, **kwargs):
98
88
if meth is None and request .method == "HEAD" :
99
89
meth = getattr (self , "get" , None )
100
90
101
- # Inject request arguments if an args schema is defined
102
- if request .method in self .arg_methods and self .get_args ():
103
- meth = use_args (self .get_args ())(meth )
104
-
105
- # Marhal response if a response schema is defined
106
- if request .method in self .marshal_methods and self .get_schema ():
107
- meth = marshal_with (self .get_schema ())(meth )
108
-
109
91
# Flask should ensure this is assersion never fails
110
92
assert meth is not None , f"Unimplemented method { request .method !r} "
111
93
@@ -133,22 +115,57 @@ def represent_response(self, response):
133
115
134
116
135
117
class ActionView (View ):
136
- _cls_tags = {"actions" }
118
+
119
+ # Data formatting
120
+ schema : Schema = None
121
+ args : dict = None
122
+ semtype : str = None
123
+
124
+ # Spec overrides
125
+ responses = {} # Custom responses for invokeaction
126
+
127
+ # Spec parameters
137
128
safe : bool = False
138
129
idempotent : bool = False
139
130
131
+ # Internal
132
+ _cls_tags = {"actions" }
133
+ _deque = Deque () # Action queue
134
+
140
135
@classmethod
141
- def get_responses (cls ):
142
- """Build an output schema that includes the Action wrapper object"""
143
- r = {
144
- 201 : {
145
- "schema" : build_action_schema (cls .schema , cls .args )(),
146
- "content_type" : "application/json" ,
147
- "description" : "Action started" ,
148
- }
136
+ def get_apispec (cls ):
137
+ d = {
138
+ "post" : {
139
+ "description" : getattr (cls , "description" , None ) or get_docstring (cls ),
140
+ "summary" : getattr (cls , "summary" , None ) or get_summary (cls ),
141
+ "tags" : list (cls .get_tags ()),
142
+ "requestBody" : {"content" : {"application/json" : cls .schema }},
143
+ "responses" : {
144
+ # Our POST 201 will usually be application/json
145
+ 201 : {
146
+ "schema" : build_action_schema (cls .schema , cls .args )(),
147
+ "content_type" : "application/json" ,
148
+ "description" : "Action started" ,
149
+ }
150
+ },
151
+ },
152
+ "get" : {
153
+ "description" : "Action queue" ,
154
+ "summary" : "Action queue" ,
155
+ "tags" : list (cls .get_tags ()),
156
+ "responses" : {
157
+ # Our GET 200 will usually be application/json
158
+ 200 : {
159
+ "schema" : build_action_schema (cls .schema , cls .args )(many = True ),
160
+ "content_type" : "application/json" ,
161
+ "description" : "Action started" ,
162
+ }
163
+ },
164
+ },
149
165
}
150
- r .update (cls .responses )
151
- return r
166
+ # Enable custom responses from POST
167
+ d ["post" ]["responses" ].update (cls .responses )
168
+ return d
152
169
153
170
def dispatch_request (self , * args , ** kwargs ):
154
171
meth = getattr (self , request .method .lower (), None )
@@ -158,12 +175,12 @@ def dispatch_request(self, *args, **kwargs):
158
175
return View .dispatch_request (self , * args , ** kwargs )
159
176
160
177
# Inject request arguments if an args schema is defined
161
- if self .get_args () :
162
- meth = use_args (self .get_args () )(meth )
178
+ if self .args :
179
+ meth = use_args (self .args )(meth )
163
180
164
- # Marhal response if a response schema is defines
165
- if self .get_schema () :
166
- meth = marshal_with (self .get_schema () )(meth )
181
+ # Marhal response if a response schema is defined
182
+ if self .schema :
183
+ meth = marshal_with (self .schema )(meth )
167
184
168
185
# Make a task out of the views `post` method
169
186
task = taskify (meth )(* args , ** kwargs )
@@ -181,27 +198,93 @@ def dispatch_request(self, *args, **kwargs):
181
198
except Timeout :
182
199
pass
183
200
201
+ # Log the action to the view's deque
202
+ self ._deque .append (task )
203
+
184
204
# If the action returns quickly, and returns a valid Response, return it as-is
185
205
if task .output and isinstance (task .output , ResponseBase ):
186
- return self .represent_response (task .output )
206
+ return self .represent_response (task .output , 200 )
187
207
188
208
return self .represent_response ((ActionSchema ().dump (task ), 201 ))
189
209
190
210
191
211
class PropertyView (View ):
212
+ schema : Schema = None
213
+ semtype : str = None
214
+
215
+ # Spec overrides
216
+ responses = {} # Custom responses for invokeaction
217
+
192
218
_cls_tags = {"properties" }
193
219
194
220
@classmethod
195
- def get_args (cls ):
196
- """Use the output schema for arguments, on Properties"""
197
- return cls .schema
221
+ def get_apispec (cls ):
222
+ d = {}
223
+
224
+ # writeproperty methods
225
+ for method in ("put" , "post" ):
226
+ if hasattr (cls , method ):
227
+ d [method ] = {
228
+ "description" : getattr (cls , "description" , None )
229
+ or get_docstring (cls ),
230
+ "summary" : getattr (cls , "summary" , None ) or get_summary (cls ),
231
+ "tags" : list (cls .get_tags ()),
232
+ "requestBody" : {
233
+ "content" : {"application/json" : {"schema" : cls .schema }}
234
+ },
235
+ "responses" : {
236
+ 200 : {
237
+ "schema" : cls .schema ,
238
+ "content_type" : "application/json" ,
239
+ "description" : "Write property" ,
240
+ }
241
+ },
242
+ }
243
+
244
+ if hasattr (cls , "get" ):
245
+ d ["get" ] = {
246
+ "description" : getattr (cls , "description" , None ) or get_docstring (cls ),
247
+ "summary" : getattr (cls , "summary" , None ) or get_summary (cls ),
248
+ "tags" : list (cls .get_tags ()),
249
+ "responses" : {
250
+ 200 : {
251
+ "schema" : cls .schema ,
252
+ "content_type" : "application/json" ,
253
+ "description" : "Read property" ,
254
+ }
255
+ },
256
+ }
257
+
258
+ # Enable custom responses from all methods
259
+ for method in d .keys ():
260
+ d [method ]["responses" ].update (cls .responses )
261
+ return d
198
262
199
263
def dispatch_request (self , * args , ** kwargs ):
264
+ meth = getattr (self , request .method .lower (), None )
265
+
266
+ # Flask should ensure this is assersion never fails
267
+ assert meth is not None , f"Unimplemented method { request .method !r} "
268
+
269
+ # If the request method is HEAD and we don't have a handler for it
270
+ # retry with GET.
271
+ if meth is None and request .method == "HEAD" :
272
+ meth = getattr (self , "get" , None )
273
+
274
+ # POST and PUT methods can be used to write properties
275
+ # In all other cases, ignore arguments
276
+ if request .method in ("PUT" , "POST" ) and self .schema :
277
+ meth = use_args (self .schema )(meth )
278
+
279
+ # All methods should serialise properties
280
+ if self .schema :
281
+ meth = marshal_with (self .schema )(meth )
282
+
200
283
# Generate basic response
201
- resp = View . dispatch_request ( self , * args , ** kwargs )
284
+ resp = self . represent_response ( meth ( * args , ** kwargs ) )
202
285
203
286
# Emit property event
204
- if request .method in ("POST" , "PUT" , "DELETE" , "PATCH" ):
287
+ if request .method in ("POST" , "PUT" ):
205
288
property_value = self .get_value ()
206
289
property_name = getattr (self , "endpoint" , None ) or getattr (
207
290
self , "__name__" , "unknown"
0 commit comments