import concurrent.futures from distutils.util import strtobool from typing import Any, Dict, List, Tuple import marshmallow from flask import Response, abort, g, jsonify, request from oto import response as oto_response from owsresponse import response from owsresponse.adaptors.flask import flaskify from playlist import config from playlist.api import app, executor REQUEST_ERROR = {"error": "unexpected error"} def format_response( data: dict = None, status_code=200, error=None ) -> Tuple[Response, int]: """Format standard flask response for all endpoints""" response_body = {} if data: response_body["data"] = data if error: response_body["error"] = error return jsonify(response_body), status_code def get_query_string_params( accepted_params: List[str], previous_params: Dict[str, Any] = None ) -> Dict[str, Any]: """parse out accepted query string params from request""" params = previous_params or {} for param in accepted_params: if request.args.get(param): params[param] = request.args.get(param) return params def get_post_request_params( post_request_params: Dict[str, Any], accepted_params: List[str], previous_params: Dict[str, Any] = None, ) -> Dict[str, Any]: """parse out accepted query string params from post request data""" params = previous_params or {} for param in accepted_params: if post_request_params.get(param): params[param] = post_request_params.get(param) return params def get_query_string_list_params( accepted_params: Dict[str, str], previous_params: Dict[str, Any] = None ) -> Dict[str, Any]: """parse out accepted query string params from request and return a list Accepts both repeated keys (``?k=a&k=b``) and comma-separated values (``?k=a,b``). Comma form lets callers keep large filters such as ``curator_country`` under the proxy query-string size limit, while repeated keys keep working unchanged. """ params = previous_params or {} for key, value in accepted_params.items(): if request.args.get(key): params[value] = [ item for raw in request.args.getlist(key) for item in raw.split(",") if item ] return params @app.errorhandler(marshmallow.exceptions.ValidationError) def handle_validation_error(error): return format_response(status_code=400, error=error.messages) @app.route(config.HEALTH_CHECK, methods=["GET"]) def health(): """Check the health of the application.""" return jsonify({"status": "ok"}) @app.errorhandler(500) def exception_handler(error): """Handle error when uncaught exception is raised. Default exception handler. Note: Exception will also be sent to Sentry if config.SENTRY is set. Returns: flask.Response: A 500 response with JSON 'code' & 'message' payload. """ message = ( "The server encountered an internal error " "and was unable to complete your request." ) g.log.exception(error) return flaskify(response.create_fatal_response(message)) @app.errorhandler(401) def unauthorised_handler(_error): return format_response(error="Unauthorised", status_code=401) @app.errorhandler(403) def forbidden_handler(_error): return format_response(error="Forbidden", status_code=403) def _format_response(response): """Handle formatting of responses from requests.""" if not hasattr(response, "status"): return response if response.status != 200: return REQUEST_ERROR return response.message def parallel(requests): """Execute requests in parallel. Provided a dict of requests, execute them with the provided id and kwargs. Return the results of these requests in a dict keyed by keys in the requests parameter. """ futures = { executor.submit(request["func"], *request["args"]): key for (key, request) in requests.items() } concurrent.futures.wait(futures) output = dict(map(lambda f: (futures[f], f.result()), futures)) # Format the responses for errors, return as oto response return oto_response.Response( dict(map(lambda kv: (kv[0], _format_response(kv[1])), output.items())) ) def separate_playlists_by_storefront(playlist_placements): playlist_placements_without_storefront = [] playlist_placements_with_storefront = [] for playlist in playlist_placements: if playlist.get("storefront") is not None: playlist_placements_with_storefront.append(playlist) else: playlist_placements_without_storefront.append(playlist) return playlist_placements_with_storefront, playlist_placements_without_storefront def get_query_boolean_params( accepted_params: List[str], previous_params: dict[str, Any] = None ) -> dict[str, Any]: """ Parse query parameters from request and convert a string representation of truth value to boolean. :param accepted_params: given list of query parameters to parse :param previous_params: parsed query string parameters :return: boolean value of string parameter """ params = previous_params or {} for param in accepted_params: value = request.args.get(param) if value: try: params[param] = bool(strtobool(value)) except ValueError: raise ValueError(f"Invalid boolean value for {param}: {value}") return params