Mercurial > p > roundup > code
comparison roundup/rest.py @ 5653:ba67e397f063
Fix string/bytes issues under python 3.
1) cgi/client.py: override cgi.FieldStorage's make_file so that file
is always created in binary/byte mode. This means that json (and
xml) are bytes not strings.
2) rest.py: try harder to find dicttoxml in roundup directory or on
sys.path. This just worked under python 2 but python 3 only
searches sys.path by default and does not search relative like
python 2.
3) rest.py: replace headers.getheader call removed from python 3 with
equivalent code.
4) rest.py: make value returned from dispatch into bytes not string.
5) test/caseinsensitivedict.py, test/test_CaseInsensitiveDict.py:
get code from stackoverflow that implements a case insensitive key
dict. So dict['foo'], dict['Foo'] are the same entry. Used for
looking up headers in mocked http rewuset header array.
6) test/rest_common.py: rework tests for etags and rest to properly
supply bytes to the called routines. Calls to s2b and b2s and use
of BytesIO and overriding make_file in cgi.FieldStorage to try to
make sure it works under python 3.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sun, 17 Mar 2019 19:28:26 -0400 |
| parents | d791c5ba5852 |
| children | 207e0f5d551c |
comparison
equal
deleted
inserted
replaced
| 5652:9689d1bf9bb0 | 5653:ba67e397f063 |
|---|---|
| 18 import time | 18 import time |
| 19 import traceback | 19 import traceback |
| 20 import re | 20 import re |
| 21 | 21 |
| 22 try: | 22 try: |
| 23 from dicttoxml import dicttoxml | 23 # if dicttoxml installed in roundup directory, use it |
| 24 from .dicttoxml import dicttoxml | |
| 24 except ImportError: | 25 except ImportError: |
| 25 dicttoxml = None | 26 try: |
| 27 # else look in sys.path | |
| 28 from dicttoxml import dicttoxml | |
| 29 except ImportError: | |
| 30 # else not supported | |
| 31 dicttoxml = None | |
| 26 | 32 |
| 27 from roundup import hyperdb | 33 from roundup import hyperdb |
| 28 from roundup import date | 34 from roundup import date |
| 29 from roundup import actions | 35 from roundup import actions |
| 30 from roundup.anypy.strings import bs2b | 36 from roundup.anypy.strings import bs2b |
| 158 def obtain_etags(headers,input): | 164 def obtain_etags(headers,input): |
| 159 '''Get ETags value from headers or payload data''' | 165 '''Get ETags value from headers or payload data''' |
| 160 etags = [] | 166 etags = [] |
| 161 if '@etag' in input: | 167 if '@etag' in input: |
| 162 etags.append(input['@etag'].value); | 168 etags.append(input['@etag'].value); |
| 163 etags.append(headers.getheader("ETag", None)) | 169 if "ETag" in headers: |
| 170 etags.append(headers["ETag"]) | |
| 164 return etags | 171 return etags |
| 165 | 172 |
| 166 def parse_accept_header(accept): | 173 def parse_accept_header(accept): |
| 167 """ | 174 """ |
| 168 Parse the Accept header *accept*, returning a list with 3-tuples of | 175 Parse the Accept header *accept*, returning a list with 3-tuples of |
| 218 if key == "q": | 225 if key == "q": |
| 219 q = float(value) | 226 q = float(value) |
| 220 else: | 227 else: |
| 221 media_params.append((key, value)) | 228 media_params.append((key, value)) |
| 222 result.append((media_type, dict(media_params), q)) | 229 result.append((media_type, dict(media_params), q)) |
| 223 result.sort(lambda x, y: -cmp(x[2], y[2])) | 230 # was: result.sort(lambda x, y: -cmp(x[2], y[2])) |
| 231 # change for python 3 support | |
| 232 result.sort(key=lambda x: x[2], reverse=True) | |
| 224 return result | 233 return result |
| 225 | 234 |
| 226 | 235 |
| 227 class Routing(object): | 236 class Routing(object): |
| 228 __route_map = {} | 237 __route_map = {} |
| 1297 """format and process the request""" | 1306 """format and process the request""" |
| 1298 # if X-HTTP-Method-Override is set, follow the override method | 1307 # if X-HTTP-Method-Override is set, follow the override method |
| 1299 headers = self.client.request.headers | 1308 headers = self.client.request.headers |
| 1300 # Never allow GET to be an unsafe operation (i.e. data changing). | 1309 # Never allow GET to be an unsafe operation (i.e. data changing). |
| 1301 # User must use POST to "tunnel" DELETE, PUT, OPTIONS etc. | 1310 # User must use POST to "tunnel" DELETE, PUT, OPTIONS etc. |
| 1302 override = headers.getheader('X-HTTP-Method-Override') | 1311 override = None |
| 1312 if 'X-HTTP-Method-Override' in headers: | |
| 1313 override = headers['X-HTTP-Method-Override'] | |
| 1303 output = None | 1314 output = None |
| 1304 if override: | 1315 if override: |
| 1305 if method.upper() != 'GET': | 1316 if method.upper() != 'GET': |
| 1306 logger.debug( | 1317 logger.debug( |
| 1307 'Method overridden from %s to %s', method, override) | 1318 'Method overridden from %s to %s', method, override) |
| 1312 logger.info( | 1323 logger.info( |
| 1313 'Ignoring X-HTTP-Method-Override for GET request on %s', | 1324 'Ignoring X-HTTP-Method-Override for GET request on %s', |
| 1314 uri) | 1325 uri) |
| 1315 | 1326 |
| 1316 # parse Accept header and get the content type | 1327 # parse Accept header and get the content type |
| 1317 accept_header = parse_accept_header(headers.getheader('Accept')) | 1328 accept_header = [] |
| 1329 if 'Accept' in headers: | |
| 1330 accept_header = parse_accept_header(headers['Accept']) | |
| 1318 accept_type = "invalid" | 1331 accept_type = "invalid" |
| 1319 for part in accept_header: | 1332 for part in accept_header: |
| 1320 if part[0] in self.__accepted_content_type: | 1333 if part[0] in self.__accepted_content_type: |
| 1321 accept_type = self.__accepted_content_type[part[0]] | 1334 accept_type = self.__accepted_content_type[part[0]] |
| 1322 | 1335 |
| 1348 ) | 1361 ) |
| 1349 | 1362 |
| 1350 # Is there an input.value with format json data? | 1363 # Is there an input.value with format json data? |
| 1351 # If so turn it into an object that emulates enough | 1364 # If so turn it into an object that emulates enough |
| 1352 # of the FieldStorge methods/props to allow a response. | 1365 # of the FieldStorge methods/props to allow a response. |
| 1353 content_type_header = headers.getheader('Content-Type', None) | 1366 content_type_header = None |
| 1367 if 'Content-Type' in headers: | |
| 1368 content_type_header = headers['Content-Type'] | |
| 1354 if type(input.value) == str and content_type_header: | 1369 if type(input.value) == str and content_type_header: |
| 1355 parsed_content_type_header = content_type_header | 1370 parsed_content_type_header = content_type_header |
| 1356 # the structure of a content-type header | 1371 # the structure of a content-type header |
| 1357 # is complex: mime-type; options(charset ...) | 1372 # is complex: mime-type; options(charset ...) |
| 1358 # for now we just accept application/json. | 1373 # for now we just accept application/json. |
| 1402 self.client.response_code = 406 | 1417 self.client.response_code = 406 |
| 1403 output = "Content type is not accepted by client" | 1418 output = "Content type is not accepted by client" |
| 1404 | 1419 |
| 1405 # Make output json end in a newline to | 1420 # Make output json end in a newline to |
| 1406 # separate from following text in logs etc.. | 1421 # separate from following text in logs etc.. |
| 1407 return output + "\n" | 1422 return bs2b(output + "\n") |
| 1408 | 1423 |
| 1409 | 1424 |
| 1410 class RoundupJSONEncoder(json.JSONEncoder): | 1425 class RoundupJSONEncoder(json.JSONEncoder): |
| 1411 """RoundupJSONEncoder overrides the default JSONEncoder to handle all | 1426 """RoundupJSONEncoder overrides the default JSONEncoder to handle all |
| 1412 types of the object without returning any error""" | 1427 types of the object without returning any error""" |
