Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
language: python
python:
- "2.6"
- "2.7"
install: "pip install -r test-requirements.txt"
script: "make test-coveralls"
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
oauthlib==0.6.0
requests-oauthlib==0.4.0
requests
81 changes: 72 additions & 9 deletions shapeways/oauth2_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -13,6 +14,28 @@
ORDERS_URL = '/orders/v1'
SINGLE_ORDER_URL = '/orders/{order_id}/v1'

# 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():
"""
Expand All @@ -34,13 +57,14 @@ def authenticate(self, client_id, client_secret):
:return: True for success, false for Failure
:rtype: bool
"""

auth_post_data = {
'grant_type': 'client_credentials'
}

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 == requests.codes.ok:
self.access_token = response.json()['access_token']
return True
print("Error: status code " + str(response.status_code))
Expand All @@ -51,18 +75,57 @@ def authenticate(self, client_id, client_secret):
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))
: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 != requests.codes.ok:
if response.status_code == requests.codes.too_many_requests:
# 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):
"""
Expand Down Expand Up @@ -137,7 +200,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):
"""
Expand All @@ -160,7 +223,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):
"""
Expand Down