Mercurial > p > roundup > code
comparison roundup/rest.py @ 8180:d02ce1d14acd
feat: issue2551068 - Provide way to retrieve file/msg data via rest endpoint.
Use Allow header to change format of /binary_content endpoint. If
Allow header for endpoint is not application/json, it will be matched
against the mime type for the file. */*, text/* are supported and will
return the native mime type if present.
Changes:
move */* mime type from static dict of supported types. It was
hardcoded to return json only. Now it can return a matching
non-json mime type for the /binary_content endpoint.
Edited some errors to explicitly add */* mime type.
Cleanups to use ', ' separation in lists of valid mime types rather
than just space separated.
Remove ETag header when sending raw content. See issue 2551375 for
background.
Doc added to rest.txt.
Small format fix up (add dash) in CHANGES.txt.
Make passing an unset/None/False accept_mime_type to
format_dispatch_output a 500 error. This used to be the fallback
to produce a 406 error after all processing had happened. It
should no longer be possible to take that code path as all 406
errors (with valid accept_mime_types) are generated before
processing takes place.
Make format_dispatch_output handle output other than json/xml so it
can send back binary_content data.
Removed a spurious client.response_code = 400 that seems to not be
used.
Tests added for all code paths.
Database setup for tests msg and file entry. This required a file
upload test to change so it doesn't look for file1 as the link
returned by the upload. Download the link and verify the data
rather than verifying the link.
Multiple formatting changes to error messages to make all lists of
valid mime types ', ' an not just space separated.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sun, 08 Dec 2024 17:22:33 -0500 |
| parents | 2967f37e73e4 |
| children | e5362f8e1808 |
comparison
equal
deleted
inserted
replaced
| 8179:53afc3ffd4a2 | 8180:d02ce1d14acd |
|---|---|
| 431 """The RestfulInstance performs REST request from the client""" | 431 """The RestfulInstance performs REST request from the client""" |
| 432 | 432 |
| 433 __default_patch_op = "replace" # default operator for PATCH method | 433 __default_patch_op = "replace" # default operator for PATCH method |
| 434 __accepted_content_type = { | 434 __accepted_content_type = { |
| 435 "application/json": "json", | 435 "application/json": "json", |
| 436 "*/*": "json", | |
| 437 } | 436 } |
| 438 __default_accept_type = "json" | 437 __default_accept_type = "json" |
| 439 | 438 |
| 440 __default_api_version = 1 | 439 __default_api_version = 1 |
| 441 __supported_api_versions = [1] | 440 __supported_api_versions = [1] |
| 2230 2) Parse Accept header obeying q values | 2229 2) Parse Accept header obeying q values |
| 2231 if header unparsible return 400 error object. | 2230 if header unparsible return 400 error object. |
| 2232 3) if empty or missing Accept header | 2231 3) if empty or missing Accept header |
| 2233 return self.__default_accept_type | 2232 return self.__default_accept_type |
| 2234 4) match and return best Accept header/version | 2233 4) match and return best Accept header/version |
| 2234 this includes matching mime types for file downloads | |
| 2235 using the binary_content property | |
| 2235 if version error found in matching type return 406 error | 2236 if version error found in matching type return 406 error |
| 2236 5) if no requested format is supported return 406 | 2237 5) if no requested format is supported return 406 |
| 2237 error | 2238 error |
| 2238 | 2239 |
| 2239 """ | 2240 """ |
| 2279 ) | 2280 ) |
| 2280 except UsageError as e: | 2281 except UsageError as e: |
| 2281 self.client.response_code = 406 | 2282 self.client.response_code = 406 |
| 2282 return (None, uri, self.error_obj( | 2283 return (None, uri, self.error_obj( |
| 2283 400, _("Unable to parse Accept Header. %(error)s. " | 2284 400, _("Unable to parse Accept Header. %(error)s. " |
| 2284 "Acceptable types: %(acceptable_types)s") % { | 2285 "Acceptable types: */*, %(acceptable_types)s") % { |
| 2285 'error': e.args[0], | 2286 'error': e.args[0], |
| 2286 'acceptable_types': " ".join(sorted( | 2287 'acceptable_types': ", ".join(sorted( |
| 2287 self.__accepted_content_type.keys()))})) | 2288 self.__accepted_content_type.keys()))})) |
| 2288 | 2289 |
| 2289 if not accept_header: | 2290 if not accept_header: |
| 2290 # we are using the default | 2291 # we are using the default |
| 2291 return (self.__default_accept_type, uri, None) | 2292 return (self.__default_accept_type, uri, None) |
| 2292 | 2293 |
| 2293 accept_type = "" | 2294 accept_type = "" |
| 2295 valid_binary_content_types = [] | |
| 2296 if uri.endswith("/binary_content"): | |
| 2297 request_path = uri | |
| 2298 request_class, request_id = request_path.split('/')[-3:-1] | |
| 2299 try: | |
| 2300 designator_type = self.db.getclass( | |
| 2301 request_class).get(request_id, "type") | |
| 2302 except (KeyError, IndexError): | |
| 2303 # class (KeyError) or | |
| 2304 # id (IndexError) does not exist | |
| 2305 # Return unknown mime type and no error. | |
| 2306 # The 400/404 error will be thrown by other code. | |
| 2307 return (None, uri, None) | |
| 2308 | |
| 2309 if designator_type: | |
| 2310 # put this first as we usually require exact mime | |
| 2311 # type match and this will be matched most often. | |
| 2312 # Also for text/* Accept header it will be returned. | |
| 2313 valid_binary_content_types.append(designator_type) | |
| 2314 | |
| 2315 if not designator_type or designator_type.startswith('text/'): | |
| 2316 # allow text/* as msg items can have empty type field | |
| 2317 # also match text/* for text/plain, text/x-rst, | |
| 2318 # text/markdown, text/html etc. | |
| 2319 valid_binary_content_types.append("text/*") | |
| 2320 | |
| 2321 # Octet-stream should be allowed for any content. | |
| 2322 # client.py sets 'X-Content-Type-Options: nosniff' | |
| 2323 # for file downloads (sendfile) via the html interface, | |
| 2324 # so we should be able to set it in this code as well. | |
| 2325 valid_binary_content_types.append("application/octet-stream") | |
| 2326 | |
| 2294 for part in accept_header: | 2327 for part in accept_header: |
| 2295 if accept_type: | 2328 if accept_type: |
| 2296 # we accepted the best match, stop searching for | 2329 # we accepted the best match, stop searching for |
| 2297 # lower quality matches. | 2330 # lower quality matches. |
| 2298 break | 2331 break |
| 2332 | |
| 2333 # check for structured rest return types (json xml) | |
| 2299 if part[0] in self.__accepted_content_type: | 2334 if part[0] in self.__accepted_content_type: |
| 2300 accept_type = self.__accepted_content_type[part[0]] | 2335 accept_type = self.__accepted_content_type[part[0]] |
| 2301 # Version order: | 2336 # Version order: |
| 2302 # 1) accept header version=X specifier | 2337 # 1) accept header version=X specifier |
| 2303 # application/vnd.x.y; version=1 | 2338 # application/vnd.x.y; version=1 |
| 2309 # to trigger @apiver parsing below | 2344 # to trigger @apiver parsing below |
| 2310 # Places that need the api_version info should | 2345 # Places that need the api_version info should |
| 2311 # use default if version = None | 2346 # use default if version = None |
| 2312 try: | 2347 try: |
| 2313 self.api_version = int(part[1]['version']) | 2348 self.api_version = int(part[1]['version']) |
| 2349 if self.api_version not in self.__supported_api_versions: | |
| 2350 raise ValueError | |
| 2314 except KeyError: | 2351 except KeyError: |
| 2315 self.api_version = None | 2352 self.api_version = None |
| 2316 except (ValueError, TypeError): | 2353 except (ValueError, TypeError): |
| 2317 self.client.response_code = 406 | 2354 self.client.response_code = 406 |
| 2318 # TypeError if int(None) | 2355 # TypeError if int(None) |
| 2321 "for supported versions.") % ( | 2358 "for supported versions.") % ( |
| 2322 part[1]['version']) | 2359 part[1]['version']) |
| 2323 return (None, uri, | 2360 return (None, uri, |
| 2324 self.error_obj(406, msg)) | 2361 self.error_obj(406, msg)) |
| 2325 | 2362 |
| 2363 if part[0] == "*/*": | |
| 2364 if valid_binary_content_types: | |
| 2365 self.client.setHeader("X-Content-Type-Options", "nosniff") | |
| 2366 accept_type = valid_binary_content_types[0] | |
| 2367 else: | |
| 2368 accept_type = "json" | |
| 2369 | |
| 2370 # check type of binary_content | |
| 2371 if part[0] in valid_binary_content_types: | |
| 2372 self.client.setHeader("X-Content-Type-Options", "nosniff") | |
| 2373 accept_type = part[0] | |
| 2374 # handle text wildcard | |
| 2375 if ((part[0] in 'text/*') and | |
| 2376 "text/*" in valid_binary_content_types): | |
| 2377 self.client.setHeader("X-Content-Type-Options", "nosniff") | |
| 2378 # use best choice of mime type, try not to use | |
| 2379 # text/* if there is a real text mime type/subtype. | |
| 2380 accept_type = valid_binary_content_types[0] | |
| 2381 | |
| 2326 # accept_type will be empty only if there is an Accept header | 2382 # accept_type will be empty only if there is an Accept header |
| 2327 # with invalid values. | 2383 # with invalid values. |
| 2328 if accept_type: | 2384 if accept_type: |
| 2329 return (accept_type, uri, None) | 2385 return (accept_type, uri, None) |
| 2330 | 2386 |
| 2331 self.client.response_code = 400 | 2387 if valid_binary_content_types: |
| 2388 return (None, uri, | |
| 2389 self.error_obj( | |
| 2390 406, | |
| 2391 _("Requested content type(s) '%s' not available.\n" | |
| 2392 "Acceptable mime types are: */*, %s") % | |
| 2393 (self.client.request.headers.get('Accept'), | |
| 2394 ", ".join(sorted( | |
| 2395 valid_binary_content_types))))) | |
| 2396 | |
| 2332 return (None, uri, | 2397 return (None, uri, |
| 2333 self.error_obj( | 2398 self.error_obj( |
| 2334 406, | 2399 406, |
| 2335 _("Requested content type(s) '%s' not available.\n" | 2400 _("Requested content type(s) '%s' not available.\n" |
| 2336 "Acceptable mime types are: %s") % | 2401 "Acceptable mime types are: */*, %s") % |
| 2337 (self.client.request.headers.get('Accept'), | 2402 (self.client.request.headers.get('Accept'), |
| 2338 ", ".join(sorted( | 2403 ", ".join(sorted( |
| 2339 self.__accepted_content_type.keys()))))) | 2404 self.__accepted_content_type.keys()))))) |
| 2340 | 2405 |
| 2341 def dispatch(self, method, uri, input): | 2406 def dispatch(self, method, uri, input): |
| 2595 else: | 2660 else: |
| 2596 output['error'][key] = str(val) | 2661 output['error'][key] = str(val) |
| 2597 | 2662 |
| 2598 output = '<?xml version="1.0" encoding="UTF-8" ?>\n' + \ | 2663 output = '<?xml version="1.0" encoding="UTF-8" ?>\n' + \ |
| 2599 b2s(dicttoxml(output, root=False)) | 2664 b2s(dicttoxml(output, root=False)) |
| 2665 elif accept_mime_type: | |
| 2666 self.client.setHeader("Content-Type", accept_mime_type) | |
| 2667 # do not send etag when getting binary_content. The ETag | |
| 2668 # is for the item not the content of the item. So the ETag | |
| 2669 # can change even though the content is the same. Since | |
| 2670 # content is immutable by default, the client shouldn't | |
| 2671 # need the etag for writing. | |
| 2672 self.client.setHeader("ETag", None) | |
| 2673 return output['data']['data'] | |
| 2600 else: | 2674 else: |
| 2601 # FIXME?? consider moving this earlier. We should | 2675 self.client.response_code = 500 |
| 2602 # error out before doing any work if we can't | 2676 output = _("Internal error while formatting response.\n" |
| 2603 # display acceptable output. | 2677 "accept_mime_type is not defined. This should\n" |
| 2604 self.client.response_code = 406 | 2678 "never happen\n") |
| 2605 output = ("Requested content type '%s' is not available.\n" | |
| 2606 "Acceptable types: %s" % (accept_mime_type, | |
| 2607 ", ".join(sorted(self.__accepted_content_type.keys())))) | |
| 2608 | 2679 |
| 2609 # Make output json end in a newline to | 2680 # Make output json end in a newline to |
| 2610 # separate from following text in logs etc.. | 2681 # separate from following text in logs etc.. |
| 2611 return bs2b(output + "\n") | 2682 return bs2b(output + "\n") |
| 2612 | 2683 |
