from datetime import datetime from decimal import Decimal from functools import wraps import io import time import flask from flask import make_response from flask import request from flask import send_file from oto import response import simplejson as json from masters_registry.constant import error from masters_registry.constant import field_const def flaskify(response, headers=None, encoder=None): """Format the response to be consumeable by flask. This function is a copy of oto.adaptors.flask.flaskify function. The only difference is that here we use "simplejson" library instead of Python's "json". That allows us to serialize decimal values. The api returns mostly JSON responses. The format method converts the dicts into a json object (as a string), and the right response is returned (with the valid mimetype, charset and status.) Args: response (Response): The dictionary object to convert into a json object. If the value is a string, a dictionary is created with the key "message". headers (dict): optional headers for the flask response. encoder (Class): The class of the encoder (if any). Returns: flask.Response: The flask response with formatted data, headers, and mimetype. """ status_code = response.status data = response.errors or response.message mimetype = 'text/plain' if isinstance(data, list) or isinstance(data, dict): mimetype = 'application/json' data = json.dumps(data, cls=encoder) return flask.Response( response=data, status=status_code, headers=headers, mimetype=mimetype) def get_orchard_user_id(request): """Trim prefix from orchard_user_id Args: request: Request object from handler. Returns: str: Cleaned orchard_user_id """ raw_data = request.headers.get(field_const.ORCHARD_USER_ID, '') return raw_data def get_response_json(response): """Try to deserialize json from requests.Response object Args: response: 'requests' response object Returns: Decoded JSON or string """ try: message = response.json() except TypeError: message = response.text except json.JSONDecodeError: message = response.text return message def send_csv_file(csv_data, correlation_id, user): """Create report response. Args: csv_data (response.Response): data source correlation_id (str): correlation ID for marking response user (int): user ID for marking response """ csv_data = csv_data.message buf = io.BytesIO() buf.write(csv_data[field_const.CONTENT].encode('utf-8')) buf.seek(0) sf_response = make_response( send_file( buf, download_name=csv_data[field_const.FILE_NAME], as_attachment=True, mimetype='text/csv')) sf_response.headers[field_const.CORRELATION_ID] = correlation_id sf_response.headers[field_const.ORCHARD_USER_ID] = user return sf_response class RetryCountExceededError(Exception): """Raised by @retry decorator when it exceeds the defined retry count """ def __str__(self): return 'Method retry count exceeded' class FlagStateError(Exception): """Raised in a case when a function is called and a feature flag has incorrect state. """ def __init__(self, flag_name, correct_state): """ Args: flag_name (str): flag name correct_state: (bool): whether the flag should be disabled or enabled for the function to work correctly """ self.flag_name = flag_name self.correct_state = correct_state def __str__(self): flag_value = 'enabled' if self.correct_state else 'disabled' return ( "This function can be used only if the '{0}' flag is {1}".format( self.flag_name, flag_value)) class OwsCarveoutError(Exception): """Raised in case of failed response from ows-carveouts in _get_from_ows_carveouts_service """ def __init__(self, status_code, message): """Create and OwsCarveouts instance Store the status_code and message from failed ows-carveouts response. Args: status_code (str): http status code of response message (str): description of failed response """ super(Exception, self).__init__(status_code, message) self.status_code = status_code self.message = message def __str__(self): """Returns a string description of error Returns: str """ return "Ows-carveouts failed" def retry(error_condition=lambda err: is_ows_carveout_failed(err), retry_count=3, retry_timeout=1, progressive_timeout=True): """Decorator for retrying function call in case of exception You could decorate any function or method with this if you need to repeatedly call this method a couple of times with an increasing interval in case of some error raised during the method call. Args: error_condition (callable(error)): Function that will check whether we should do retries for a particular error. E.g. you can check error class, some it's fields or values. retry_count (int): Number ot retries retry_timeout (int): Timeout in seconds to wait between retries progressive_timeout (bool): If True, the timeout value will be increased by 0.5 sec during each consecutive retry Raises: RetryCountExceededError: Error is being raised in case of retry count exceeded """ def wrapper(fn): @wraps(fn) def wrapped(*args, **kwargs): retries = 0 timeout = retry_timeout while retries < retry_count: try: result = fn(*args, **kwargs) return result except Exception as err: if error_condition(err): time.sleep(timeout) retries += 1 timeout += 0.5 if progressive_timeout else 0 else: raise err raise RetryCountExceededError return wrapped return wrapper def is_ows_carveout_failed(err): """Checks if passed error is OwsCarveouts. And stored status_code if 504 Args: err (OwsCarveoutError): instance of OwsCarveouts Returns: bool: Whether exception is response and reason contains information, that service returned timeouts """ if type(err) is not OwsCarveoutError: return False if err.status_code == 504: return True def get_timestamp_for_dynamo(): """Returns string utcnow timestamp to save in dynamo.""" return Decimal(str(datetime.utcnow().timestamp())) def load_json_from_request(): """Load json from flask request. Returns: response.Response: result or errors """ try: payload = request.get_json(silent=True) # maybe Content-Type header is missing if not payload: payload = json.loads(request.data.decode()) if not payload: raise ValueError(error.EMPTY_PAYLOAD_MESSAGE) return response.Response(payload) except ValueError as e: return response.create_error_response( code=error.BAD_REQUEST_CODE, message=e.args[0])