Mercurial > p > roundup > code
comparison roundup/rest.py @ 6638:e1588ae185dc issue2550923_computed_property
merge from default branch. Fix travis.ci so CI builds don't error out
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 21 Apr 2022 16:54:17 -0400 |
| parents | 01a5dd90286e |
| children | 9a1f5e496e6c |
comparison
equal
deleted
inserted
replaced
| 6508:85db90cc1705 | 6638:e1588ae185dc |
|---|---|
| 121 data['@stats'] = self.db.stats | 121 data['@stats'] = self.db.stats |
| 122 result = { | 122 result = { |
| 123 'data': data | 123 'data': data |
| 124 } | 124 } |
| 125 return result | 125 return result |
| 126 | |
| 127 format_object.wrapped_func = func | |
| 126 return format_object | 128 return format_object |
| 127 | 129 |
| 130 def openapi_doc(d): | |
| 131 """Annotate rest routes with openapi data. Takes a dict | |
| 132 for the openapi spec. It can be used standalone | |
| 133 as the openapi spec paths.<path>.<method> = | |
| 134 | |
| 135 { | |
| 136 "summary": "this path gets a value", | |
| 137 "description": "a longer description", | |
| 138 "responses": { | |
| 139 "200": { | |
| 140 "description": "normal response", | |
| 141 "content": { | |
| 142 "application/json": {}, | |
| 143 "application/xml": {} | |
| 144 } | |
| 145 }, | |
| 146 "406": { | |
| 147 "description": "Unable to provide requested content type", | |
| 148 "content": { | |
| 149 "application/json": {} | |
| 150 } | |
| 151 } | |
| 152 }, | |
| 153 "parameters": [ | |
| 154 { | |
| 155 "$ref": "#components/parameters/generic_.stats" | |
| 156 }, | |
| 157 { | |
| 158 "$ref": "#components/parameters/generic_.apiver" | |
| 159 }, | |
| 160 { | |
| 161 "$ref": "#components/parameters/generic_.verbose" | |
| 162 } | |
| 163 ] | |
| 164 } | |
| 165 """ | |
| 166 | |
| 167 def wrapper(f): | |
| 168 f.openapi_doc = d | |
| 169 return f | |
| 170 return wrapper | |
| 128 | 171 |
| 129 def calculate_etag(node, key, classname="Missing", id="0", | 172 def calculate_etag(node, key, classname="Missing", id="0", |
| 130 repr_format="json"): | 173 repr_format="json"): |
| 131 '''given a hyperdb node generate a hashed representation of it to be | 174 '''given a hyperdb node generate a hashed representation of it to be |
| 132 used as an etag. | 175 used as an etag. |
| 173 | 216 |
| 174 node_etag = calculate_etag(node, key, classname, id, | 217 node_etag = calculate_etag(node, key, classname, id, |
| 175 repr_format=repr_format) | 218 repr_format=repr_format) |
| 176 | 219 |
| 177 for etag in etags: | 220 for etag in etags: |
| 178 if etag is not None: | 221 # etag includes doublequotes around tag: |
| 179 if etag != node_etag: | 222 # '"a46a5572190e4fad63958c135f3746fa"' |
| 223 # but can include content-encoding suffix like: | |
| 224 # '"a46a5572190e4fad63958c135f3746fa-gzip"' | |
| 225 # turn the latter into the former as we don't care what | |
| 226 # encoding was used to send the body with the etag. | |
| 227 try: | |
| 228 suffix_start = etag.rindex('-') | |
| 229 clean_etag = etag[:suffix_start] + '"' | |
| 230 except (ValueError, AttributeError): | |
| 231 # - not in etag or etag is None | |
| 232 clean_etag = etag | |
| 233 if clean_etag is not None: | |
| 234 if clean_etag != node_etag: | |
| 180 return False | 235 return False |
| 181 have_etag_match = True | 236 have_etag_match = True |
| 182 | 237 |
| 183 if have_etag_match: | 238 if have_etag_match: |
| 184 return True | 239 return True |
| 345 match_obj = path_regex.match(path) | 400 match_obj = path_regex.match(path) |
| 346 if match_obj: | 401 if match_obj: |
| 347 try: | 402 try: |
| 348 func_obj = funcs[method] | 403 func_obj = funcs[method] |
| 349 except KeyError: | 404 except KeyError: |
| 350 raise Reject('Method %s not allowed' % method) | 405 valid_methods = ', '.join(sorted(funcs.keys())) |
| 406 raise Reject(_('Method %(m)s not allowed. ' | |
| 407 'Allowed: %(a)s')% { | |
| 408 'm': method, | |
| 409 'a': valid_methods | |
| 410 }, | |
| 411 valid_methods) | |
| 351 | 412 |
| 352 # retrieve the vars list and the function caller | 413 # retrieve the vars list and the function caller |
| 353 list_vars = func_obj['vars'] | 414 list_vars = func_obj['vars'] |
| 354 func = func_obj['func'] | 415 func = func_obj['func'] |
| 355 | 416 |
| 500 if '.' in p: | 561 if '.' in p: |
| 501 prop = None | 562 prop = None |
| 502 for pn in p.split('.'): | 563 for pn in p.split('.'): |
| 503 # Tried to dereference a non-Link property | 564 # Tried to dereference a non-Link property |
| 504 if cn is None: | 565 if cn is None: |
| 505 raise AttributeError("Unknown: %s" % p) | 566 raise UsageError("Property %(base)s can not be dereferenced in %(p)s." % { "base": p[:-(len(pn)+1)], "p": p}) |
| 506 cls = self.db.getclass(cn) | 567 cls = self.db.getclass(cn) |
| 507 # This raises a KeyError for unknown prop: | 568 # This raises a KeyError for unknown prop: |
| 508 try: | 569 try: |
| 509 prop = cls.getprops(protected=True)[pn] | 570 prop = cls.getprops(protected=True)[pn] |
| 510 except KeyError: | 571 except KeyError: |
| 511 raise AttributeError("Unknown: %s" % p) | 572 raise KeyError("Unknown property: %s" % p) |
| 512 if isinstance(prop, hyperdb.Multilink): | 573 if isinstance(prop, hyperdb.Multilink): |
| 513 raise UsageError( | 574 raise UsageError( |
| 514 'Multilink Traversal not allowed: %s' % p) | 575 'Multilink Traversal not allowed: %s' % p) |
| 515 # Now we have the classname in cn and the prop name in pn. | 576 # Now we have the classname in cn and the prop name in pn. |
| 516 if not self.db.security.hasPermission('View', uid, cn, pn): | 577 if not self.db.security.hasPermission('View', uid, cn, pn): |
| 519 % (cn, pn))) | 580 % (cn, pn))) |
| 520 try: | 581 try: |
| 521 cn = prop.classname | 582 cn = prop.classname |
| 522 except AttributeError: | 583 except AttributeError: |
| 523 cn = None | 584 cn = None |
| 585 else: | |
| 586 cls = self.db.getclass(cn) | |
| 587 # This raises a KeyError for unknown prop: | |
| 588 try: | |
| 589 prop = cls.getprops(protected=True)[pn] | |
| 590 except KeyError: | |
| 591 raise KeyError("Unknown property: %s" % pn) | |
| 524 checked_props.append (p) | 592 checked_props.append (p) |
| 525 return checked_props | 593 return checked_props |
| 526 | 594 |
| 527 def error_obj(self, status, msg, source=None): | 595 def error_obj(self, status, msg, source=None): |
| 528 """Return an error object""" | 596 """Return an error object""" |
| 739 verbose = int(value) | 807 verbose = int(value) |
| 740 elif key == "@fields" or key == "@attrs": | 808 elif key == "@fields" or key == "@attrs": |
| 741 f = value.split(",") | 809 f = value.split(",") |
| 742 if len(f) == 1: | 810 if len(f) == 1: |
| 743 f = value.split(":") | 811 f = value.split(":") |
| 744 allprops = class_obj.getprops(protected=True) | |
| 745 display_props.update(self.transitive_props(class_name, f)) | 812 display_props.update(self.transitive_props(class_name, f)) |
| 746 elif key == "@sort": | 813 elif key == "@sort": |
| 747 f = value.split(",") | 814 f = value.split(",") |
| 748 allprops = class_obj.getprops(protected=True) | |
| 749 for p in f: | 815 for p in f: |
| 750 if not p: | 816 if not p: |
| 751 raise UsageError("Empty property " | 817 raise UsageError("Empty property " |
| 752 "for class %s." % (class_name)) | 818 "for class %s." % (class_name)) |
| 753 if p[0] in ('-', '+'): | 819 if p[0] in ('-', '+'): |
| 782 try: | 848 try: |
| 783 prop = class_obj.getprops()[p] | 849 prop = class_obj.getprops()[p] |
| 784 except KeyError: | 850 except KeyError: |
| 785 raise UsageError("Field %s is not valid for %s class." % | 851 raise UsageError("Field %s is not valid for %s class." % |
| 786 (p, class_name)) | 852 (p, class_name)) |
| 853 # Call this for the side effect of validating the key | |
| 854 # use _discard as _ is apparently a global for the translation | |
| 855 # service. | |
| 856 _discard = self.transitive_props(class_name, [ key ]) | |
| 787 # We drop properties without search permission silently | 857 # We drop properties without search permission silently |
| 788 # This reflects the current behavior of other roundup | 858 # This reflects the current behavior of other roundup |
| 789 # interfaces | 859 # interfaces |
| 790 # Note that hasSearchPermission already returns 0 for | 860 # Note that hasSearchPermission already returns 0 for |
| 791 # non-existing properties. | 861 # non-existing properties. |
| 889 for field in input.value | 959 for field in input.value |
| 890 if field.name != "@page_index"])}) | 960 if field.name != "@page_index"])}) |
| 891 | 961 |
| 892 result['@total_size'] = result_len | 962 result['@total_size'] = result_len |
| 893 self.client.setHeader("X-Count-Total", str(result_len)) | 963 self.client.setHeader("X-Count-Total", str(result_len)) |
| 964 self.client.setHeader("Allow", "OPTIONS, GET, POST") | |
| 894 return 200, result | 965 return 200, result |
| 895 | 966 |
| 896 @Routing.route("/data/<:class_name>/<:item_id>", 'GET') | 967 @Routing.route("/data/<:class_name>/<:item_id>", 'GET') |
| 897 @_data_decorator | 968 @_data_decorator |
| 898 def get_element(self, class_name, item_id, input): | 969 def get_element(self, class_name, item_id, input): |
| 960 props = set() | 1031 props = set() |
| 961 # support , or : separated elements | 1032 # support , or : separated elements |
| 962 f = value.split(",") | 1033 f = value.split(",") |
| 963 if len(f) == 1: | 1034 if len(f) == 1: |
| 964 f = value.split(":") | 1035 f = value.split(":") |
| 965 allprops = class_obj.getprops(protected=True) | |
| 966 props.update(self.transitive_props(class_name, f)) | 1036 props.update(self.transitive_props(class_name, f)) |
| 967 elif key == "@protected": | 1037 elif key == "@protected": |
| 968 # allow client to request read only | 1038 # allow client to request read only |
| 969 # properties like creator, activity etc. | 1039 # properties like creator, activity etc. |
| 970 # used only if no @fields/@attrs | 1040 # used only if no @fields/@attrs |
| 1026 | 1096 |
| 1027 class_obj = self.db.getclass(class_name) | 1097 class_obj = self.db.getclass(class_name) |
| 1028 node = class_obj.getnode(item_id) | 1098 node = class_obj.getnode(item_id) |
| 1029 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY, | 1099 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY, |
| 1030 class_name, item_id, repr_format="json") | 1100 class_name, item_id, repr_format="json") |
| 1031 data = node.__getattr__(attr_name) | 1101 try: |
| 1102 data = node.__getattr__(attr_name) | |
| 1103 except AttributeError as e: | |
| 1104 raise UsageError(_("Invalid attribute %s"%attr_name)) | |
| 1032 result = { | 1105 result = { |
| 1033 'id': item_id, | 1106 'id': item_id, |
| 1034 'type': str(type(data)), | 1107 'type': str(type(data)), |
| 1035 'link': "%s/%s/%s/%s" % | 1108 'link': "%s/%s/%s/%s" % |
| 1036 (self.data_path, class_name, item_id, attr_name), | 1109 (self.data_path, class_name, item_id, attr_name), |
| 1186 raise UsageError("Must provide the %s property." % msg) | 1259 raise UsageError("Must provide the %s property." % msg) |
| 1187 | 1260 |
| 1188 # set the header Location | 1261 # set the header Location |
| 1189 link = '%s/%s/%s' % (self.data_path, class_name, item_id) | 1262 link = '%s/%s/%s' % (self.data_path, class_name, item_id) |
| 1190 self.client.setHeader("Location", link) | 1263 self.client.setHeader("Location", link) |
| 1264 | |
| 1265 self.client.setHeader( | |
| 1266 "Allow", | |
| 1267 None | |
| 1268 ) | |
| 1269 self.client.setHeader( | |
| 1270 "Access-Control-Allow-Methods", | |
| 1271 None | |
| 1272 ) | |
| 1191 | 1273 |
| 1192 # set the response body | 1274 # set the response body |
| 1193 result = { | 1275 result = { |
| 1194 'id': item_id, | 1276 'id': item_id, |
| 1195 'link': link | 1277 'link': link |
| 1639 """ | 1721 """ |
| 1640 if class_name not in self.db.classes: | 1722 if class_name not in self.db.classes: |
| 1641 raise NotFound('Class %s not found' % class_name) | 1723 raise NotFound('Class %s not found' % class_name) |
| 1642 self.client.setHeader( | 1724 self.client.setHeader( |
| 1643 "Allow", | 1725 "Allow", |
| 1726 "OPTIONS, GET, POST" | |
| 1727 ) | |
| 1728 | |
| 1729 self.client.setHeader( | |
| 1730 "Access-Control-Allow-Methods", | |
| 1644 "OPTIONS, GET, POST" | 1731 "OPTIONS, GET, POST" |
| 1645 ) | 1732 ) |
| 1646 return 204, "" | 1733 return 204, "" |
| 1647 | 1734 |
| 1648 @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS') | 1735 @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS') |
| 1696 else: | 1783 else: |
| 1697 raise NotFound('Attribute %s not valid for Class %s' % ( | 1784 raise NotFound('Attribute %s not valid for Class %s' % ( |
| 1698 attr_name, class_name)) | 1785 attr_name, class_name)) |
| 1699 return 204, "" | 1786 return 204, "" |
| 1700 | 1787 |
| 1788 @openapi_doc({"summary": "Describe Roundup rest endpoint.", | |
| 1789 "description": ("Report all supported api versions " | |
| 1790 "and default api version. " | |
| 1791 "Also report next level of link " | |
| 1792 "endpoints below /rest endpoint"), | |
| 1793 "responses": { | |
| 1794 "200": { | |
| 1795 "description": "Successful response.", | |
| 1796 "content": { | |
| 1797 "application/json": { | |
| 1798 "examples": { | |
| 1799 "success": { | |
| 1800 "summary": "Normal json data.", | |
| 1801 "value": """{ | |
| 1802 "data": { | |
| 1803 "default_version": 1, | |
| 1804 "supported_versions": [ | |
| 1805 1 | |
| 1806 ], | |
| 1807 "links": [ | |
| 1808 { | |
| 1809 "uri": "https://tracker.example.com/demo/rest", | |
| 1810 "rel": "self" | |
| 1811 }, | |
| 1812 { | |
| 1813 "uri": "https://tracker.example.com/demo/rest/data", | |
| 1814 "rel": "data" | |
| 1815 }, | |
| 1816 { | |
| 1817 "uri": "https://tracker.example.com/demo/rest/summary", | |
| 1818 "rel": "summary" | |
| 1819 } | |
| 1820 ] | |
| 1821 } | |
| 1822 }""" | |
| 1823 } | |
| 1824 } | |
| 1825 }, | |
| 1826 "application/xml": { | |
| 1827 "examples": { | |
| 1828 "success": { | |
| 1829 "summary": "Normal xml data", | |
| 1830 "value": """<dataf type="dict"> | |
| 1831 <default_version type="int">1</default_version> | |
| 1832 <supported_versions type="list"> | |
| 1833 <item type="int">1</item> | |
| 1834 </supported_versions> | |
| 1835 <links type="list"> | |
| 1836 <item type="dict"> | |
| 1837 <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest</uri> | |
| 1838 <rel type="str">self</rel> | |
| 1839 </item> | |
| 1840 <item type="dict"> | |
| 1841 <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/data</uri> | |
| 1842 <rel type="str">data</rel> | |
| 1843 </item> | |
| 1844 <item type="dict"> | |
| 1845 <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/summary</uri> | |
| 1846 <rel type="str">summary</rel> | |
| 1847 </item> | |
| 1848 <item type="dict"> | |
| 1849 <uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/summary2</uri> | |
| 1850 <rel type="str">summary2</rel> | |
| 1851 </item> | |
| 1852 </links> | |
| 1853 </dataf>""" | |
| 1854 } | |
| 1855 } | |
| 1856 } | |
| 1857 } | |
| 1858 } | |
| 1859 } | |
| 1860 } | |
| 1861 ) | |
| 1701 @Routing.route("/") | 1862 @Routing.route("/") |
| 1702 @_data_decorator | 1863 @_data_decorator |
| 1703 def describe(self, input): | 1864 def describe(self, input): |
| 1704 """Describe the rest endpoint""" | 1865 """Describe the rest endpoint. Return direct children in |
| 1866 links list. | |
| 1867 """ | |
| 1868 | |
| 1869 # paths looks like ['^rest/$', '^rest/summary$', | |
| 1870 # '^rest/data/<:class>$', ...] | |
| 1871 paths = Routing._Routing__route_map.keys() | |
| 1872 | |
| 1873 links = [] | |
| 1874 # p[1:-1] removes ^ and $ from regexp | |
| 1875 # if p has only 1 /, it's a child of rest/ root. | |
| 1876 child_paths = sorted([ p[1:-1] for p in paths if | |
| 1877 p.count('/') == 1 ]) | |
| 1878 for p in child_paths: | |
| 1879 # p.split('/')[1] is the residual path after | |
| 1880 # removing rest/. child_paths look like: | |
| 1881 # ['rest/', 'rest/summary'] etc. | |
| 1882 rel = p.split('/')[1] | |
| 1883 if rel: | |
| 1884 rel_path = "/" + rel | |
| 1885 else: | |
| 1886 rel_path = rel | |
| 1887 rel = "self" | |
| 1888 links.append( {"uri": self.base_path + rel_path, | |
| 1889 "rel": rel | |
| 1890 }) | |
| 1891 | |
| 1705 result = { | 1892 result = { |
| 1706 "default_version": self.__default_api_version, | 1893 "default_version": self.__default_api_version, |
| 1707 "supported_versions": self.__supported_api_versions, | 1894 "supported_versions": self.__supported_api_versions, |
| 1708 "links": [{"uri": self.base_path + "/summary", | 1895 "links": links |
| 1709 "rel": "summary"}, | |
| 1710 {"uri": self.base_path, | |
| 1711 "rel": "self"}, | |
| 1712 {"uri": self.base_path + "/data", | |
| 1713 "rel": "data"}] | |
| 1714 } | 1896 } |
| 1715 | 1897 |
| 1716 return 200, result | 1898 return 200, result |
| 1717 | 1899 |
| 1718 @Routing.route("/", 'OPTIONS') | 1900 @Routing.route("/", 'OPTIONS') |
| 1947 self.api_version = int(part[1]['version']) | 2129 self.api_version = int(part[1]['version']) |
| 1948 except KeyError: | 2130 except KeyError: |
| 1949 self.api_version = None | 2131 self.api_version = None |
| 1950 except (ValueError, TypeError): | 2132 except (ValueError, TypeError): |
| 1951 # TypeError if int(None) | 2133 # TypeError if int(None) |
| 1952 msg = ("Unrecognized version: %s. " | 2134 msg = ("Unrecognized api version: %s. " |
| 1953 "See /rest without specifying version " | 2135 "See /rest without specifying api version " |
| 1954 "for supported versions." % ( | 2136 "for supported versions." % ( |
| 1955 part[1]['version'])) | 2137 part[1]['version'])) |
| 1956 output = self.error_obj(400, msg) | 2138 output = self.error_obj(400, msg) |
| 1957 | 2139 |
| 1958 # get the request format for response | 2140 # get the request format for response |
| 1959 # priority : extension from uri (/rest/data/issue.json), | 2141 # priority : extension from uri (/rest/data/issue.json), |
| 1960 # header (Accept: application/json, application/xml) | 2142 # header (Accept: application/json, application/xml) |
| 1961 # default (application/json) | 2143 # default (application/json) |
| 1962 ext_type = os.path.splitext(urlparse(uri).path)[1][1:] | 2144 ext_type = os.path.splitext(urlparse(uri).path)[1][1:] |
| 1963 | 2145 |
| 2146 # Check to see if the length of the extension is less than 6. | |
| 2147 # this allows use of .vcard for a future use in downloading | |
| 2148 # user info. It also allows passing through larger items like | |
| 2149 # JWT that has a final component > 6 items. This method also | |
| 2150 # allow detection of mistyped types like jon for json. | |
| 2151 if ext_type and (len(ext_type) < 6): | |
| 2152 # strip extension so uri make sense | |
| 2153 # .../issue.json -> .../issue | |
| 2154 uri = uri[:-(len(ext_type) + 1)] | |
| 2155 else: | |
| 2156 ext_type = None | |
| 2157 | |
| 1964 # headers.get('Accept') is never empty if called here. | 2158 # headers.get('Accept') is never empty if called here. |
| 1965 # accept_type will be set to json if there is no Accept header | 2159 # accept_type will be set to json if there is no Accept header |
| 1966 # accept_type wil be empty only if there is an Accept header | 2160 # accept_type wil be empty only if there is an Accept header |
| 1967 # with invalid values. | 2161 # with invalid values. |
| 1968 data_type = ext_type or accept_type or headers.get('Accept') or "invalid" | 2162 data_type = ext_type or accept_type or headers.get('Accept') or "invalid" |
| 1969 | 2163 |
| 1970 if (ext_type): | |
| 1971 # strip extension so uri make sense | |
| 1972 # .../issue.json -> .../issue | |
| 1973 uri = uri[:-(len(ext_type) + 1)] | |
| 1974 | |
| 1975 # add access-control-allow-* to support CORS | 2164 # add access-control-allow-* to support CORS |
| 1976 self.client.setHeader("Access-Control-Allow-Origin", "*") | 2165 self.client.setHeader("Access-Control-Allow-Origin", "*") |
| 1977 self.client.setHeader( | 2166 self.client.setHeader( |
| 1978 "Access-Control-Allow-Headers", | 2167 "Access-Control-Allow-Headers", |
| 1979 "Content-Type, Authorization, X-HTTP-Method-Override" | 2168 "Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override" |
| 1980 ) | 2169 ) |
| 1981 self.client.setHeader( | 2170 self.client.setHeader( |
| 1982 "Allow", | 2171 "Allow", |
| 1983 "OPTIONS, GET, POST, PUT, DELETE, PATCH" | 2172 "OPTIONS, GET, POST, PUT, DELETE, PATCH" |
| 1984 ) | 2173 ) |
| 1985 self.client.setHeader( | 2174 self.client.setHeader( |
| 1986 "Access-Control-Allow-Methods", | 2175 "Access-Control-Allow-Methods", |
| 1987 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" | 2176 "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH" |
| 1988 ) | 2177 ) |
| 1989 # Is there an input.value with format json data? | 2178 # Is there an input.value with format json data? |
| 1990 # If so turn it into an object that emulates enough | 2179 # If so turn it into an object that emulates enough |
| 1991 # of the FieldStorge methods/props to allow a response. | 2180 # of the FieldStorge methods/props to allow a response. |
| 1992 content_type_header = headers.get('Content-Type', None) | 2181 content_type_header = headers.get('Content-Type', None) |
| 2021 except (KeyError, TypeError): | 2210 except (KeyError, TypeError): |
| 2022 pretty_output = True | 2211 pretty_output = True |
| 2023 | 2212 |
| 2024 # check for runtime statistics | 2213 # check for runtime statistics |
| 2025 try: | 2214 try: |
| 2215 # self.report_stats initialized to False | |
| 2026 self.report_stats = input['@stats'].value.lower() == "true" | 2216 self.report_stats = input['@stats'].value.lower() == "true" |
| 2027 # Can also return a TypeError ("not indexable") | 2217 # Can also return a TypeError ("not indexable") |
| 2028 # In case the FieldStorage could not parse the result | 2218 # In case the FieldStorage could not parse the result |
| 2029 except (KeyError, TypeError): | 2219 except (KeyError, TypeError): |
| 2030 report_stats = False | 2220 pass |
| 2031 | 2221 |
| 2032 # check for @apiver in query string | 2222 # check for @apiver in query string |
| 2033 msg = ("Unrecognized version: %s. " | 2223 msg = _("Unrecognized api version: %s. " |
| 2034 "See /rest without specifying version " | 2224 "See /rest without specifying api version " |
| 2035 "for supported versions.") | 2225 "for supported versions.") |
| 2036 try: | 2226 try: |
| 2037 if not self.api_version: | 2227 if not self.api_version: |
| 2038 self.api_version = int(input['@apiver'].value) | 2228 self.api_version = int(input['@apiver'].value) |
| 2039 # Can also return a TypeError ("not indexable") | 2229 # Can also return a TypeError ("not indexable") |
| 2050 # version? This may be a good thing to require. Note that: | 2240 # version? This may be a good thing to require. Note that: |
| 2051 # Accept: application/json; version=1 may not be legal but.... | 2241 # Accept: application/json; version=1 may not be legal but.... |
| 2052 # Use default if not specified for now. | 2242 # Use default if not specified for now. |
| 2053 self.api_version = self.__default_api_version | 2243 self.api_version = self.__default_api_version |
| 2054 elif self.api_version not in self.__supported_api_versions: | 2244 elif self.api_version not in self.__supported_api_versions: |
| 2055 raise UsageError(msg % self.api_version) | 2245 output = self.error_obj(400, msg % self.api_version) |
| 2056 | 2246 |
| 2057 # sadly del doesn't work on FieldStorage which can be the type of | 2247 # sadly del doesn't work on FieldStorage which can be the type of |
| 2058 # input. So we have to ignore keys starting with @ at other | 2248 # input. So we have to ignore keys starting with @ at other |
| 2059 # places in the code. | 2249 # places in the code. |
| 2060 # else: | 2250 # else: |
| 2067 if not output: | 2257 if not output: |
| 2068 output = Routing.execute(self, uri, method, input) | 2258 output = Routing.execute(self, uri, method, input) |
| 2069 except NotFound as msg: | 2259 except NotFound as msg: |
| 2070 output = self.error_obj(404, msg) | 2260 output = self.error_obj(404, msg) |
| 2071 except Reject as msg: | 2261 except Reject as msg: |
| 2072 output = self.error_obj(405, msg) | 2262 output = self.error_obj(405, msg.args[0]) |
| 2263 self.client.setHeader("Allow", msg.args[1]) | |
| 2073 | 2264 |
| 2074 # Format the content type | 2265 # Format the content type |
| 2075 if data_type.lower() == "json": | 2266 if data_type.lower() == "json": |
| 2076 self.client.setHeader("Content-Type", "application/json") | 2267 self.client.setHeader("Content-Type", "application/json") |
| 2077 if pretty_output: | 2268 if pretty_output: |
