Mercurial > p > roundup > code
diff roundup/rest.py @ 6525:c505c774a94d
Mutiple changes to REST code.
Requesting an invalid attribut via rest/data/class/id/attrib used to
return a 405, it now returns a 400 and a better error message.
/rest/ response scans the registered endpoints rather than using a
hard coded description. So new endpoints added in interfaces.py are
listed.
Fix a number of Allow headers that were listing invalid methods. Also
when invalid method is used, report valid methods in response. Extract
methods from Route list.
Fix Access-Control-Allow-Methods. Add X-Requested-With to
Access-Control-Allow-Headers.
Add decorator openapi_doc to add openapi annotations for the rest
endpoints. Added a couple of examples. Returning this info to a
client is still a work in progress.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sun, 07 Nov 2021 01:04:43 -0500 |
| parents | a22ea1a7e92c |
| children | f8df7fed18f6 |
line wrap: on
line diff
--- a/roundup/rest.py Sun Nov 07 01:49:03 2021 -0400 +++ b/roundup/rest.py Sun Nov 07 01:04:43 2021 -0500 @@ -123,8 +123,51 @@ 'data': data } return result + + format_object.wrapped_func = func return format_object +def openapi_doc(d): + """Annotate rest routes with openapi data. Takes a dict + for the openapi spec. It can be used standalone + as the openapi spec paths.<path>.<method> = + + { + "summary": "this path gets a value", + "description": "a longer description", + "responses": { + "200": { + "description": "normal response", + "content": { + "application/json": {}, + "application/xml": {} + } + }, + "406": { + "description": "Unable to provide requested content type", + "content": { + "application/json": {} + } + } + }, + "parameters": [ + { + "$ref": "#components/parameters/generic_.stats" + }, + { + "$ref": "#components/parameters/generic_.apiver" + }, + { + "$ref": "#components/parameters/generic_.verbose" + } + ] + } + """ + + def wrapper(f): + f.openapi_doc = d + return f + return wrapper def calculate_etag(node, key, classname="Missing", id="0", repr_format="json"): @@ -347,7 +390,13 @@ try: func_obj = funcs[method] except KeyError: - raise Reject('Method %s not allowed' % method) + valid_methods = ', '.join(sorted(funcs.keys())) + raise Reject(_('Method %(m)s not allowed. ' + 'Allowed: %(a)s')% { + 'm': method, + 'a': valid_methods + }, + valid_methods) # retrieve the vars list and the function caller list_vars = func_obj['vars'] @@ -891,6 +940,7 @@ result['@total_size'] = result_len self.client.setHeader("X-Count-Total", str(result_len)) + self.client.setHeader("Allow", "OPTIONS, GET, POST") return 200, result @Routing.route("/data/<:class_name>/<:item_id>", 'GET') @@ -1028,7 +1078,10 @@ node = class_obj.getnode(item_id) etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY, class_name, item_id, repr_format="json") - data = node.__getattr__(attr_name) + try: + data = node.__getattr__(attr_name) + except AttributeError as e: + raise UsageError(_("Invalid attribute %s"%attr_name)) result = { 'id': item_id, 'type': str(type(data)), @@ -1189,6 +1242,15 @@ link = '%s/%s/%s' % (self.data_path, class_name, item_id) self.client.setHeader("Location", link) + self.client.setHeader( + "Allow", + None + ) + self.client.setHeader( + "Access-Control-Allow-Methods", + None + ) + # set the response body result = { 'id': item_id, @@ -1643,6 +1705,11 @@ "Allow", "OPTIONS, GET, POST" ) + + self.client.setHeader( + "Access-Control-Allow-Methods", + "OPTIONS, GET, POST" + ) return 204, "" @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS') @@ -1698,19 +1765,114 @@ attr_name, class_name)) return 204, "" + @openapi_doc({"summary": "Describe Roundup rest endpoint.", + "description": ("Report all supported api versions " + "and default api version. " + "Also report next level of link " + "endpoints below /rest endpoint"), + "responses": { + "200": { + "description": "Successful response.", + "content": { + "application/json": { + "examples": { + "success": { + "summary": "Normal json data.", + "value": """{ + "data": { + "default_version": 1, + "supported_versions": [ + 1 + ], + "links": [ + { + "uri": "https://tracker.example.com/demo/rest", + "rel": "self" + }, + { + "uri": "https://tracker.example.com/demo/rest/data", + "rel": "data" + }, + { + "uri": "https://tracker.example.com/demo/rest/summary", + "rel": "summary" + } + ] + } +}""" + } + } + }, + "application/xml": { + "examples": { + "success": { + "summary": "Normal xml data", + "value": """<dataf type="dict"> + <default_version type="int">1</default_version> + <supported_versions type="list"> + <item type="int">1</item> + </supported_versions> + <links type="list"> + <item type="dict"> + <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest</uri> + <rel type="str">self</rel> + </item> + <item type="dict"> + <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/data</uri> + <rel type="str">data</rel> + </item> + <item type="dict"> + <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/summary</uri> + <rel type="str">summary</rel> + </item> + <item type="dict"> + <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/summary2</uri> + <rel type="str">summary2</rel> + </item> + </links> +</dataf>""" + } + } + } + } + } + } + } + ) @Routing.route("/") @_data_decorator def describe(self, input): - """Describe the rest endpoint""" + """Describe the rest endpoint. Return direct children in + links list. + """ + + # paths looks like ['^rest/$', '^rest/summary$', + # '^rest/data/<:class>$', ...] + paths = Routing._Routing__route_map.keys() + + links = [] + # p[1:-1] removes ^ and $ from regexp + # if p has only 1 /, it's a child of rest/ root. + child_paths = sorted([ p[1:-1] for p in paths if + p.count('/') == 1 ]) + for p in child_paths: + # p.split('/')[1] is the residual path after + # removing rest/. child_paths look like: + # ['rest/', 'rest/summary'] etc. + rel = p.split('/')[1] + if rel: + rel_path = "/" + rel + else: + rel_path = rel + rel = "self" + links.append( {"uri": self.base_path + rel_path, + "rel": rel + }) + result = { "default_version": self.__default_api_version, "supported_versions": self.__supported_api_versions, - "links": [{"uri": self.base_path + "/summary", - "rel": "summary"}, - {"uri": self.base_path, - "rel": "self"}, - {"uri": self.base_path + "/data", - "rel": "data"}] + "links": links } return 200, result @@ -1983,7 +2145,7 @@ self.client.setHeader("Access-Control-Allow-Origin", "*") self.client.setHeader( "Access-Control-Allow-Headers", - "Content-Type, Authorization, X-HTTP-Method-Override" + "Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override" ) self.client.setHeader( "Allow", @@ -1991,7 +2153,7 @@ ) self.client.setHeader( "Access-Control-Allow-Methods", - "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" + "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH" ) # Is there an input.value with format json data? # If so turn it into an object that emulates enough @@ -2076,7 +2238,8 @@ except NotFound as msg: output = self.error_obj(404, msg) except Reject as msg: - output = self.error_obj(405, msg) + output = self.error_obj(405, msg.args[0]) + self.client.setHeader("Allow", msg.args[1]) # Format the content type if data_type.lower() == "json":
