def _post_data(url: str, data_: typing.Dict, network_manager: QgsNetworkAccessManager, auth_config: str, feedback: typing.Optional[QgsFeedback] = None): request = QNetworkRequest(QUrl(url)) request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/json') reply = network_manager.blockingPost(request, json.dumps(data_).encode('utf-8'), auth_config, True, feedback=feedback) status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) raw_string_contents = bytes(reply.content()).decode('utf-8') if status_code == 201: result = json.loads(raw_string_contents) else: raise QgsProcessingException(f'POST request failed. ' f'status_code: {status_code} - ' f'error_string: {reply.errorString()} - ' f'reply_contents: {raw_string_contents}') return result
class Client(QObject): """Performs requests to the ORS API services.""" def __init__(self, provider=None, retry_timeout=60): """ :param provider: A openrouteservice provider from config.yml :type provider: dict :param retry_timeout: Timeout across multiple retriable requests, in seconds. :type retry_timeout: int """ QObject.__init__(self) self.key = provider['key'] self.base_url = provider['base_url'] # self.session = requests.Session() self.nam = QgsNetworkAccessManager() self.retry_timeout = timedelta(seconds=retry_timeout) self.headers = { "User-Agent": _USER_AGENT, 'Content-type': 'application/json', } # Save some references to retrieve in client instances self.url = None self.warnings = None self.response_time = 0 self.status_code = None overQueryLimit = pyqtSignal() def request(self, url, first_request_time=None, retry_counter=0, post_json=None): """Performs HTTP GET/POST with credentials, returning the body as JSON. :param url: URL extension for request. Should begin with a slash. :type url: string :param first_request_time: The time of the first request (None if no retries have occurred). :type first_request_time: datetime.datetime :param post_json: Parameters for POST endpoints :type post_json: dict :raises valhalla.utils.exceptions.ApiError: when the API returns an error. :returns: openrouteservice response body :rtype: dict """ if not first_request_time: first_request_time = datetime.now() elapsed = datetime.now() - first_request_time if elapsed > self.retry_timeout: raise exceptions.Timeout() if retry_counter > 0: # 0.5 * (1.5 ^ i) is an increased sleep time of 1.5x per iteration, # starting at 0.5s when retry_counter=1. The first retry will occur # at 1, so subtract that first. delay_seconds = 1.5**(retry_counter - 1) # Jitter this value by 50% and pause. time.sleep(delay_seconds * (random.random() + 0.5)) # Define the request params = {'access_token': self.key} authed_url = self._generate_auth_url( url, params, ) url_object = QUrl(self.base_url + authed_url) self.url = url_object.url() body = QJsonDocument.fromJson(json.dumps(post_json).encode()) request = QNetworkRequest(url_object) request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/json') logger.log( "url: {}\nParameters: {}".format( self.url, # final_requests_kwargs json.dumps(post_json, indent=2)), 0) start = time.time() response: QgsNetworkReplyContent = self.nam.blockingPost( request, body.toJson()) self.response_time = time.time() - start try: self.handle_response(response, post_json['id']) except exceptions.OverQueryLimit: # Let the instances know smth happened self.overQueryLimit.emit() return self.request(url, first_request_time, retry_counter + 1, post_json) response_content = json.loads(bytes(response.content())) # Mapbox treats 400 errors with a 200 status code if 'error' in response_content: raise exceptions.ApiError(str(response_content['status_code']), response_content['error']) return response_content def handle_response(self, response, feat_id): """ Casts JSON response to dict :raises valhalla.utils.exceptions.OverQueryLimitError: when rate limit is exhausted, HTTP 429 :raises valhalla.utils.exceptions.ApiError: when the backend API throws an error, HTTP 400 :raises valhalla.utils.exceptions.InvalidKey: when API key is invalid (or quota is exceeded), HTTP 403 :raises valhalla.utils.exceptions.GenericServerError: all other HTTP errors :returns: response body :rtype: dict """ self.status_code = response.attribute( QNetworkRequest.HttpStatusCodeAttribute) if response.error(): # First try non-HTTP error codes error_code = response.error() error_msg = response.errorString() if error_code in (QNetworkReply.ConnectionRefusedError, QNetworkReply.HostNotFoundError): raise exceptions.GenericServerError( 1, f"Host {self.base_url} not valid.") elif error_code == QNetworkReply.TimeoutError: raise exceptions.Timeout("Request timed out.") if self.status_code == 401: raise exceptions.InvalidKey(str(self.status_code), error_msg) elif self.status_code == 429: logger.log("{}: {}".format(exceptions.OverQueryLimit.__name__, "Query limit exceeded")) raise exceptions.OverQueryLimit(str(429), error_msg) # Internal error message for Bad Request elif self.status_code and 400 <= self.status_code < 500: logger.log("Feature ID {} caused a {}: {}".format( feat_id, exceptions.ApiError.__name__, error_msg, 2)) raise exceptions.ApiError(str(self.status_code), error_msg) else: raise exceptions.GenericServerError(str(self.status_code), error_msg) def _generate_auth_url(self, path, params): """Returns the path and query string portion of the request URL, first adding any necessary parameters. :param path: The path portion of the URL. :type path: string :param params: URL parameters. :type params: dict or list of key/value tuples :returns: encoded URL :rtype: string """ if type(params) is dict: params = sorted(dict(**params).items()) # Only auto-add API key when using ORS. If own instance, API key must # be explicitly added to params # if self.key: # params.append(("api_key", self.key)) return path + "?" + requests.utils.unquote_unreserved( urlencode(params))