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"""

Roundup Issue Tracker: http://roundup-tracker.org/