Mercurial > p > roundup > code
changeset 5597:de9933cfcfc4 REST-rebased
Added routing decorator
committer: Ralf Schlatterbeck <rsc@runtux.com>
| author | Chau Nguyen <dangchau1991@yahoo.com> |
|---|---|
| date | Wed, 30 Jan 2019 10:26:35 +0100 |
| parents | 55fa81de6a57 |
| children | be81e8cca38c |
| files | roundup/rest.py |
| diffstat | 1 files changed, 133 insertions(+), 58 deletions(-) [+] |
line wrap: on
line diff
--- a/roundup/rest.py Wed Jan 30 10:26:35 2019 +0100 +++ b/roundup/rest.py Wed Jan 30 10:26:35 2019 +0100 @@ -12,10 +12,12 @@ import sys import time import traceback -import xml +import re + from roundup import hyperdb from roundup import date from roundup.exceptions import * +from roundup.cgi.exceptions import * def _data_decorator(func): @@ -24,6 +26,9 @@ # get the data / error from function try: code, data = func(self, *args, **kwargs) + except NotFound, msg: + code = 404 + data = msg except IndexError, msg: code = 404 data = msg @@ -133,6 +138,84 @@ return result +class Routing(object): + __route_map = {} + __var_to_regex = re.compile(r"<:(\w+)>") + url_to_regex = r"([\w.\-~!$&'()*+,;=:@\%%]+)" + + @classmethod + def route(cls, rule, methods='GET'): + """A decorator that is used to register a view function for a + given URL rule: + @self.route('/') + def index(): + return 'Hello World' + + rest/ will be added to the beginning of the url string + + Args: + rule (string): the URL rule + methods (string or tuple or list): the http method + """ + # strip the '/' character from rule string + rule = rule.strip('/') + + # add 'rest/' to the rule string + if not rule.startswith('rest/'): + rule = '^rest/' + rule + '$' + + if isinstance(methods, basestring): # convert string to tuple + methods = (methods,) + methods = set(item.upper() for item in methods) + + # convert a rule to a compiled regex object + # so /data/<:class>/<:id> will become + # /data/([charset]+)/([charset]+) + # and extract the variable names to a list [(class), (id)] + func_vars = cls.__var_to_regex.findall(rule) + rule = re.compile(cls.__var_to_regex.sub(cls.url_to_regex, rule)) + + # then we decorate it: + # route_map[regex][method] = func + def decorator(func): + rule_route = cls.__route_map.get(rule, {}) + func_obj = { + 'func': func, + 'vars': func_vars + } + for method in methods: + rule_route[method] = func_obj + cls.__route_map[rule] = rule_route + return func + return decorator + + @classmethod + def execute(cls, instance, path, method, input): + # format the input + path = path.strip('/').lower() + method = method.upper() + + # find the rule match the path + # then get handler match the method + for path_regex in cls.__route_map: + match_obj = path_regex.match(path) + if match_obj: + try: + func_obj = cls.__route_map[path_regex][method] + except KeyError: + raise Reject('Method %s not allowed' % method) + + # retrieve the vars list and the function caller + list_vars = func_obj['vars'] + func = func_obj['func'] + + # zip the varlist into a dictionary, and pass it to the caller + args = dict(zip(list_vars, match_obj.groups())) + args['input'] = input + return func(instance, **args) + raise NotFound('Nothing matches the given URI') + + class RestfulInstance(object): """The RestfulInstance performs REST request from the client""" @@ -280,6 +363,7 @@ return result + @Routing.route("/data/<:class_name>", 'GET') @_data_decorator def get_collection(self, class_name, input): """GET resource from class URI. @@ -297,6 +381,8 @@ id: id of the object link: path to the object """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'View', self.db.getuid(), class_name ): @@ -348,6 +434,7 @@ self.client.setHeader("X-Count-Total", str(len(result))) return 200, result + @Routing.route("/data/<:class_name>/<:item_id>", 'GET') @_data_decorator def get_element(self, class_name, item_id, input): """GET resource from object URI. @@ -368,6 +455,8 @@ link: link to the object attributes: a dictionary represent the attributes of the object """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'View', self.db.getuid(), class_name, itemid=item_id ): @@ -394,6 +483,7 @@ return 200, result + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'GET') @_data_decorator def get_attribute(self, class_name, item_id, attr_name, input): """GET resource from attribute URI. @@ -415,6 +505,8 @@ link: link to the attribute data: data of the requested attribute """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'View', self.db.getuid(), class_name, attr_name, item_id ): @@ -435,6 +527,7 @@ return 200, result + @Routing.route("/data/<:class_name>", 'POST') @_data_decorator def post_collection(self, class_name, input): """POST a new object to a class @@ -452,6 +545,8 @@ id: id of the object link: path to the object """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'Create', self.db.getuid(), class_name ): @@ -495,21 +590,7 @@ } return 201, result - @_data_decorator - def post_element(self, class_name, item_id, input): - """POST to an object of a class is not allowed""" - raise Reject('POST to an item is not allowed') - - @_data_decorator - def post_attribute(self, class_name, item_id, attr_name, input): - """POST to an attribute of an object is not allowed""" - raise Reject('POST to an attribute is not allowed') - - @_data_decorator - def put_collection(self, class_name, input): - """PUT a class is not allowed""" - raise Reject('PUT a class is not allowed') - + @Routing.route("/data/<:class_name>/<:item_id>", 'PUT') @_data_decorator def put_element(self, class_name, item_id, input): """PUT a new content to an object @@ -530,6 +611,8 @@ attributes: a dictionary represent only changed attributes of the object """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) class_obj = self.db.getclass(class_name) props = self.props_from_args(class_obj, input.value, item_id) @@ -555,6 +638,7 @@ } return 200, result + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PUT') @_data_decorator def put_attribute(self, class_name, item_id, attr_name, input): """PUT an attribute to an object @@ -574,6 +658,8 @@ attributes: a dictionary represent only changed attributes of the object """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'Edit', self.db.getuid(), class_name, attr_name, item_id ): @@ -604,6 +690,7 @@ return 200, result + @Routing.route("/data/<:class_name>", 'DELETE') @_data_decorator def delete_collection(self, class_name, input): """DELETE all objects in a class @@ -618,6 +705,8 @@ status (string): 'ok' count (int): number of deleted objects """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'Delete', self.db.getuid(), class_name ): @@ -644,6 +733,7 @@ return 200, result + @Routing.route("/data/<:class_name>/<:item_id>", 'DELETE') @_data_decorator def delete_element(self, class_name, item_id, input): """DELETE an object in a class @@ -658,6 +748,8 @@ dict: status (string): 'ok' """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'Delete', self.db.getuid(), class_name, itemid=item_id ): @@ -673,6 +765,7 @@ return 200, result + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'DELETE') @_data_decorator def delete_attribute(self, class_name, item_id, attr_name, input): """DELETE an attribute in a object by setting it to None or empty @@ -688,6 +781,8 @@ dict: status (string): 'ok' """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) if not self.db.security.hasPermission( 'Edit', self.db.getuid(), class_name, attr_name, item_id ): @@ -716,11 +811,7 @@ return 200, result - @_data_decorator - def patch_collection(self, class_name, input): - """PATCH a class is not allowed""" - raise Reject('PATCH a class is not allowed') - + @Routing.route("/data/<:class_name>/<:item_id>", 'PATCH') @_data_decorator def patch_element(self, class_name, item_id, input): """PATCH an object @@ -744,6 +835,8 @@ attributes: a dictionary represent only changed attributes of the object """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) try: op = input['op'].value.lower() except KeyError: @@ -779,6 +872,7 @@ } return 200, result + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PATCH') @_data_decorator def patch_attribute(self, class_name, item_id, attr_name, input): """PATCH an attribute of an object @@ -803,6 +897,8 @@ attributes: a dictionary represent only changed attributes of the object """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) try: op = input['op'].value.lower() except KeyError: @@ -842,6 +938,7 @@ } return 200, result + @Routing.route("/data/<:class_name>", 'OPTIONS') @_data_decorator def options_collection(self, class_name, input): """OPTION return the HTTP Header for the class uri @@ -850,8 +947,11 @@ int: http status code 204 (No content) body (string): an empty string """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) return 204, "" + @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS') @_data_decorator def options_element(self, class_name, item_id, input): """OPTION return the HTTP Header for the object uri @@ -860,12 +960,15 @@ int: http status code 204 (No content) body (string): an empty string """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) self.client.setHeader( "Accept-Patch", "application/x-www-form-urlencoded, multipart/form-data" ) return 204, "" + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'OPTIONS') @_data_decorator def option_attribute(self, class_name, item_id, attr_name, input): """OPTION return the HTTP Header for the attribute uri @@ -874,12 +977,15 @@ int: http status code 204 (No content) body (string): an empty string """ + if class_name not in self.db.classes: + raise NotFound('Class %s not found' % class_name) self.client.setHeader( "Accept-Patch", "application/x-www-form-urlencoded, multipart/form-data" ) return 204, "" + @Routing.route("/summary") @_data_decorator def summary(self, input): """Get a summary of resource from class URI. @@ -983,44 +1089,13 @@ "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" ) - # PATH is split to multiple pieces - # 0 - rest - # 1 - data - # 2 - resource - # 3 - attribute - uri_split = uri.lower().split("/") - # Call the appropriate method - if len(uri_split) == 2 and uri_split[1] == 'summary': - output = self.summary(input) - elif 4 >= len(uri_split) > 2 and uri_split[1] == 'data': - resource_uri = uri_split[2] - try: - class_name, item_id = hyperdb.splitDesignator(resource_uri) - except hyperdb.DesignatorError: - class_name = resource_uri - item_id = None - - if class_name not in self.db.classes: - output = self.error_obj(404, "Not found") - elif item_id is None: - if len(uri_split) == 3: - output = getattr( - self, "%s_collection" % method.lower() - )(class_name, input) - else: - output = self.error_obj(404, "Not found") - else: - if len(uri_split) == 3: - output = getattr( - self, "%s_element" % method.lower() - )(class_name, item_id, input) - else: - output = getattr( - self, "%s_attribute" % method.lower() - )(class_name, item_id, uri_split[3], input) - else: - output = self.error_obj(404, "Not found") + try: + output = Routing.execute(self, uri, method, input) + except NotFound, msg: + output = self.error_obj(404, msg) + except Reject, msg: + output = self.error_obj(405, msg) # Format the content type if data_type.lower() == "json":
