From b35b26f70eb0f26027bb3721a6472c423149815d Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 18 Nov 2019 13:41:31 -0500 Subject: [PATCH 1/4] First step --- shapeways/oauth2_client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shapeways/oauth2_client.py b/shapeways/oauth2_client.py index c05ac07..823a995 100644 --- a/shapeways/oauth2_client.py +++ b/shapeways/oauth2_client.py @@ -13,6 +13,9 @@ ORDERS_URL = '/orders/v1' SINGLE_ORDER_URL = '/orders/{order_id}/v1' +HTTP_OK = 200 +HTTP_RATE_LIMITED = 429 + class ShapewaysOauth2Client(): """ @@ -40,7 +43,7 @@ def authenticate(self, client_id, client_secret): response = requests.post(url=self.api_url + AUTH_URL, data=auth_post_data, auth=(client_id, client_secret)) - if response.status_code == 200: + if response.status_code == HTTP_OK: self.access_token = response.json()['access_token'] return True print("Error: status code " + str(response.status_code)) @@ -53,8 +56,10 @@ def _validate_response(self, response): Internal function - validate results :rtype: list() """ - if response.status_code != 200: - raise RuntimeError("Call threw status {}".format(response.status_code)) + if response.status_code != HTTP_OK: + if response.status_code == HTTP_RATE_LIMITED: + raise RuntimeError("Rate Limited: please honor backoff time") + return RuntimeError("Error: {}".format(response.status_code)) content = response.json() try: if content['result'] == 'success': From b8e1f721ed9d41514cb2bba411be681ac971923a Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 22 Nov 2019 15:09:26 -0500 Subject: [PATCH 2/4] Add rate limiting information to client responses. --- requirements.txt | 1 + shapeways/oauth2_client.py | 76 ++++++++++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 233e88e..54213a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ oauthlib==0.6.0 requests-oauthlib==0.4.0 +requests \ No newline at end of file diff --git a/shapeways/oauth2_client.py b/shapeways/oauth2_client.py index 823a995..28a9d51 100644 --- a/shapeways/oauth2_client.py +++ b/shapeways/oauth2_client.py @@ -2,6 +2,7 @@ import json import requests +# API URLs AUTH_URL = '/oauth2/token' MATERIALS_URL = '/materials/v1' SINGLE_MATERIAL_URL = '/materials/{material_id}/v1' @@ -13,9 +14,32 @@ ORDERS_URL = '/orders/v1' SINGLE_ORDER_URL = '/orders/{order_id}/v1' +# HTTP status codes HTTP_OK = 200 HTTP_RATE_LIMITED = 429 +# Relevant headers +SW_RATE_LIMIT = "SHAPEWAYS" +CF_RATE_LIMIT = "CLOUDFLARE" +SW_RATE_LIMIT_LIMIT = 'x-ratelimit-limit' +SW_RATE_LIMIT_REMAINING = 'x-ratelimit-remaining' +SW_RATE_LIMIT_RETRY = 'x-ratelimit-retry-inseconds' +SW_RATE_LIMIT_WINDOW = 'x-ratelimit-window-inseconds' +CF_RATE_LIMIT_RETRY = 'retry-after' + +# Constants +CONTENT_SUCCESS = 'success' +CONTENT_RATE_LIMIT = 'rate_limiting' + + +class RateLimitingHeaders(): + def __init__(self, rate_limit_type=None, rate_limit_retry_inseconds=None, rate_limit_remaining=None, + is_rate_limited=None): + self.rate_limit_type = rate_limit_type + self.rate_limit_retry_inseconds = rate_limit_retry_inseconds + self.rate_limit_remaining = rate_limit_remaining + self.is_rate_limited = is_rate_limited + class ShapewaysOauth2Client(): """ @@ -37,6 +61,7 @@ def authenticate(self, client_id, client_secret): :return: True for success, false for Failure :rtype: bool """ + auth_post_data = { 'grant_type': 'client_credentials' } @@ -54,20 +79,57 @@ def authenticate(self, client_id, client_secret): def _validate_response(self, response): """ Internal function - validate results - :rtype: list() + :rtype dict() """ + + # Default values - these will be modified as needed by the rate limiting error handling + rate_limit = RateLimitingHeaders( + rate_limit_type=SW_RATE_LIMIT, + is_rate_limited=False + ) + headers = response.headers + + # Handle HTTP errors if response.status_code != HTTP_OK: if response.status_code == HTTP_RATE_LIMITED: - raise RuntimeError("Rate Limited: please honor backoff time") - return RuntimeError("Error: {}".format(response.status_code)) + # We're rate limited + rate_limit.is_rate_limited=True + + if CF_RATE_LIMIT_RETRY in headers.keys(): + # Limited by CF - backoff for a number of seconds + rate_limit.rate_limit_type=CF_RATE_LIMIT + rate_limit.rate_limit_remaining=0 + rate_limit.rate_limit_retry_inseconds=headers[CF_RATE_LIMIT_RETRY] + else: + # Shapeways Rate limiting - stupidly, we move the retryInSeconds entry from the response headers + # to the body. Dealing with this here. + rate_limit.rate_limit_remaining = 0 + rate_limit.rate_limit_retry_inseconds = response.json()['rateLimit']['retryInSeconds'] + + return {CONTENT_SUCCESS: False, CONTENT_RATE_LIMIT: rate_limit.__dict__} + else: + # Generic error + content = response.json() + content[CONTENT_SUCCESS] = False + content[CONTENT_RATE_LIMIT] = rate_limit.__dict__ + return content + + # No HTTP error - this means that we'll definitey have these headers + rate_limit.rate_limit_remaining = headers[SW_RATE_LIMIT_REMAINING] + rate_limit.rate_limit_retry_inseconds = headers[SW_RATE_LIMIT_RETRY] + content = response.json() + content[CONTENT_RATE_LIMIT] = rate_limit.__dict__ try: if content['result'] == 'success': + content[CONTENT_SUCCESS] = True return content else: - raise RuntimeError(content) + content[CONTENT_SUCCESS] = False + return content except: - raise RuntimeError(content) + content[CONTENT_SUCCESS] = False + return content def _execute_get(self, url, **params): """ @@ -142,7 +204,7 @@ def get_materials(self): :rtype: list() """ content = self._execute_get(url=self.api_url + MATERIALS_URL) - return content['materials'] + return content def get_single_material(self, material_id): """ @@ -165,7 +227,7 @@ def get_models(self, page_count=1): :rtype: list() """ content = self._execute_get(url=self.api_url + MODEL_URL + '?page=' + str(page_count)) - return content['models'] + return content def get_single_model(self, model_id): """ From 090309608cba5726c5dddb2a5fe7f3d74c344fde Mon Sep 17 00:00:00 2001 From: Matthew Boyle Date: Fri, 22 Nov 2019 16:29:28 -0500 Subject: [PATCH 3/4] We don't support python 2.6 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f38f3ce..180fc0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.6" - "2.7" install: "pip install -r test-requirements.txt" script: "make test-coveralls" From d3dc8c7dad1d8bda1a73dc4e8c315904523a7e1d Mon Sep 17 00:00:00 2001 From: Matthew Boyle Date: Fri, 22 Nov 2019 17:17:06 -0500 Subject: [PATCH 4/4] Use requests status codes, 'cause consistency --- shapeways/oauth2_client.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/shapeways/oauth2_client.py b/shapeways/oauth2_client.py index 28a9d51..b59db1f 100644 --- a/shapeways/oauth2_client.py +++ b/shapeways/oauth2_client.py @@ -14,10 +14,6 @@ ORDERS_URL = '/orders/v1' SINGLE_ORDER_URL = '/orders/{order_id}/v1' -# HTTP status codes -HTTP_OK = 200 -HTTP_RATE_LIMITED = 429 - # Relevant headers SW_RATE_LIMIT = "SHAPEWAYS" CF_RATE_LIMIT = "CLOUDFLARE" @@ -68,7 +64,7 @@ def authenticate(self, client_id, client_secret): response = requests.post(url=self.api_url + AUTH_URL, data=auth_post_data, auth=(client_id, client_secret)) - if response.status_code == HTTP_OK: + if response.status_code == requests.codes.ok: self.access_token = response.json()['access_token'] return True print("Error: status code " + str(response.status_code)) @@ -90,8 +86,8 @@ def _validate_response(self, response): headers = response.headers # Handle HTTP errors - if response.status_code != HTTP_OK: - if response.status_code == HTTP_RATE_LIMITED: + if response.status_code != requests.codes.ok: + if response.status_code == requests.codes.too_many_requests: # We're rate limited rate_limit.is_rate_limited=True