1- from flask .views import MethodView
1+ from flask .views import MethodView , http_method_funcs
22from flask import request
33from werkzeug .wrappers import Response as ResponseBase
44from werkzeug .exceptions import BadRequest
88from .args import use_args
99from .marshalling import marshal_with
1010
11- from ..utilities import unpack
11+ from ..utilities import unpack , get_docstring , get_summary
1212from ..representations import DEFAULT_REPRESENTATIONS
1313from ..find import current_labthing
1414from ..event import PropertyStatusEvent
1515from ..schema import Schema , ActionSchema , build_action_schema
1616from ..tasks import taskify
17+ from ..deque import Deque , resize_deque
1718
1819from gevent .timeout import Timeout
1920
@@ -28,31 +29,15 @@ class View(MethodView):
2829 get(), put(), post(), and delete(), corresponding to HTTP methods.
2930
3031 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
3632 """
3733
3834 endpoint = None
39-
40- schema : Schema = None
41- args : dict = None
4235 semtype : str = None
36+
4337 tags : list = [] # Custom tags the user can add
4438 _cls_tags = set () # Class tags that shouldn't be removed
4539 title : None
4640
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-
5641 def __init__ (self , * args , ** kwargs ):
5742 MethodView .__init__ (self , * args , ** kwargs )
5843
@@ -61,18 +46,23 @@ def __init__(self, *args, **kwargs):
6146 self .representations = OrderedDict (DEFAULT_REPRESENTATIONS )
6247
6348 @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
7666
7767 @classmethod
7868 def get_tags (cls ):
@@ -98,14 +88,6 @@ def dispatch_request(self, *args, **kwargs):
9888 if meth is None and request .method == "HEAD" :
9989 meth = getattr (self , "get" , None )
10090
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-
10991 # Flask should ensure this is assersion never fails
11092 assert meth is not None , f"Unimplemented method { request .method !r} "
11193
@@ -133,22 +115,57 @@ def represent_response(self, response):
133115
134116
135117class 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
137128 safe : bool = False
138129 idempotent : bool = False
139130
131+ # Internal
132+ _cls_tags = {"actions" }
133+ _deque = Deque () # Action queue
134+
140135 @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+ },
149165 }
150- r .update (cls .responses )
151- return r
166+ # Enable custom responses from POST
167+ d ["post" ]["responses" ].update (cls .responses )
168+ return d
152169
153170 def dispatch_request (self , * args , ** kwargs ):
154171 meth = getattr (self , request .method .lower (), None )
@@ -158,12 +175,12 @@ def dispatch_request(self, *args, **kwargs):
158175 return View .dispatch_request (self , * args , ** kwargs )
159176
160177 # 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 )
163180
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 )
167184
168185 # Make a task out of the views `post` method
169186 task = taskify (meth )(* args , ** kwargs )
@@ -181,27 +198,93 @@ def dispatch_request(self, *args, **kwargs):
181198 except Timeout :
182199 pass
183200
201+ # Log the action to the view's deque
202+ self ._deque .append (task )
203+
184204 # If the action returns quickly, and returns a valid Response, return it as-is
185205 if task .output and isinstance (task .output , ResponseBase ):
186- return self .represent_response (task .output )
206+ return self .represent_response (task .output , 200 )
187207
188208 return self .represent_response ((ActionSchema ().dump (task ), 201 ))
189209
190210
191211class PropertyView (View ):
212+ schema : Schema = None
213+ semtype : str = None
214+
215+ # Spec overrides
216+ responses = {} # Custom responses for invokeaction
217+
192218 _cls_tags = {"properties" }
193219
194220 @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
198262
199263 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+
200283 # Generate basic response
201- resp = View . dispatch_request ( self , * args , ** kwargs )
284+ resp = self . represent_response ( meth ( * args , ** kwargs ) )
202285
203286 # Emit property event
204- if request .method in ("POST" , "PUT" , "DELETE" , "PATCH" ):
287+ if request .method in ("POST" , "PUT" ):
205288 property_value = self .get_value ()
206289 property_name = getattr (self , "endpoint" , None ) or getattr (
207290 self , "__name__" , "unknown"
0 commit comments