Import upstream snapshot 80e710700aac9573a2230f74f7ce9e094833a0bc
Upstream: https://github.com/Rhoban/onshape-to-robot Upstream-Commit: 80e710700aac9573a2230f74f7ce9e094833a0bc Upstream-Branch: master
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
'''
|
||||
onshape
|
||||
======
|
||||
|
||||
Provides access to the Onshape REST API
|
||||
'''
|
||||
|
||||
from . import utils
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import commentjson as json
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import urllib
|
||||
import datetime
|
||||
import requests
|
||||
from colorama import Fore, Back, Style
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
__all__ = [
|
||||
'Onshape'
|
||||
]
|
||||
|
||||
|
||||
class Onshape():
|
||||
'''
|
||||
Provides access to the Onshape REST API.
|
||||
|
||||
Attributes:
|
||||
- stack (str): Base URL
|
||||
- creds (str, default='./creds.json'): Credentials location
|
||||
- logging (bool, default=True): Turn logging on or off
|
||||
'''
|
||||
|
||||
def __init__(self, stack, creds='./config.json', logging=True):
|
||||
'''
|
||||
Instantiates an instance of the Onshape class. Reads credentials from a JSON file
|
||||
of this format:
|
||||
|
||||
{
|
||||
"http://cad.onshape.com": {
|
||||
"access_key": "YOUR KEY HERE",
|
||||
"secret_key": "YOUR KEY HERE"
|
||||
},
|
||||
etc... add new object for each stack to test on
|
||||
}
|
||||
|
||||
The creds.json file should be stored in the root project folder; optionally,
|
||||
you can specify the location of a different file.
|
||||
|
||||
Args:
|
||||
- stack (str): Base URL
|
||||
- creds (str, default='./config.json'): Credentials location
|
||||
'''
|
||||
|
||||
if not os.path.isfile(creds):
|
||||
raise IOError('%s is not a file' % creds)
|
||||
|
||||
self._logging = logging
|
||||
|
||||
with open(creds, "r", encoding="utf-8") as stream:
|
||||
try:
|
||||
config = json.load(stream)
|
||||
except TypeError as ex:
|
||||
raise ValueError('%s is not valid json' % creds) from ex
|
||||
|
||||
try:
|
||||
# Trying to retrieve from config.json, this is deprecated but kept for backwards compatibility
|
||||
self._url = config["onshape_api"]
|
||||
self._access_key = config['onshape_access_key'].encode('utf-8')
|
||||
self._secret_key = config['onshape_secret_key'].encode('utf-8')
|
||||
|
||||
print(Fore.YELLOW + 'WARNING: Storing Onshape credentials in config.json is deprecated, please use environment variables instead' + Style.RESET_ALL)
|
||||
except KeyError:
|
||||
self._url = os.getenv('ONSHAPE_API')
|
||||
self._secret_bearer = os.getenv('ONSHAPE_SECRET_BEARER')
|
||||
self._access_key = os.getenv('ONSHAPE_ACCESS_KEY')
|
||||
self._secret_key = os.getenv('ONSHAPE_SECRET_KEY')
|
||||
|
||||
if self._url and self._secret_bearer:
|
||||
self._secret_bearer = self._secret_bearer.encode('utf-8')
|
||||
elif self._url and self._access_key and self._secret_key:
|
||||
self._access_key = self._access_key.encode('utf-8')
|
||||
self._secret_key = self._secret_key.encode('utf-8')
|
||||
else:
|
||||
print(Fore.RED + 'ERROR: No Onshape API access key are set' + Style.RESET_ALL)
|
||||
print()
|
||||
print(Fore.BLUE + 'TIP: Connect to https://dev-portal.onshape.com/keys, and edit your .bashrc file:' + Style.RESET_ALL)
|
||||
print(Fore.BLUE + 'export ONSHAPE_API=https://cad.onshape.com' + Style.RESET_ALL)
|
||||
print(Fore.BLUE + 'export ONSHAPE_ACCESS_KEY=Your_Access_Key' + Style.RESET_ALL)
|
||||
print(Fore.BLUE + 'export ONSHAPE_SECRET_KEY=Your_Secret_Key' + Style.RESET_ALL)
|
||||
exit(1)
|
||||
|
||||
if self._logging:
|
||||
utils.log('onshape instance created: url = %s, access key = %s' % (self._url, self._access_key))
|
||||
|
||||
def _make_nonce(self):
|
||||
'''
|
||||
Generate a unique ID for the request, 25 chars in length
|
||||
|
||||
Returns:
|
||||
- str: Cryptographic nonce
|
||||
'''
|
||||
|
||||
chars = string.digits + string.ascii_letters
|
||||
nonce = ''.join(random.choice(chars) for i in range(25))
|
||||
|
||||
if self._logging:
|
||||
utils.log('nonce created: %s' % nonce)
|
||||
|
||||
return nonce
|
||||
|
||||
def _append_auth(self, output_headers: dict, method, date, path, query={}, ctype='application/json'):
|
||||
'''
|
||||
Create the request signature to authenticate
|
||||
|
||||
Args:
|
||||
- method (str): HTTP method
|
||||
- date (str): HTTP date header string
|
||||
- nonce (str): Cryptographic nonce
|
||||
- path (str): URL pathname
|
||||
- query (dict, default={}): URL query string in key-value pairs
|
||||
- ctype (str, default='application/json'): HTTP Content-Type
|
||||
'''
|
||||
|
||||
if self._secret_bearer:
|
||||
# Access using simple bearer token
|
||||
output_headers['Authorization'] = 'Bearer ' + self._secret_bearer.decode('utf-8')
|
||||
else:
|
||||
# Access using API key
|
||||
nonce = self._make_nonce()
|
||||
query = urllib.parse.urlencode(query)
|
||||
|
||||
hmac_str = (method + '\n' + nonce + '\n' + date + '\n' + ctype + '\n' + path +
|
||||
'\n' + query + '\n').lower().encode('utf-8')
|
||||
|
||||
signature = base64.b64encode(hmac.new(self._secret_key, hmac_str, digestmod=hashlib.sha256).digest())
|
||||
auth = 'On ' + self._access_key.decode('utf-8') + ':HmacSHA256:' + signature.decode('utf-8')
|
||||
|
||||
if self._logging:
|
||||
utils.log({
|
||||
'query': query,
|
||||
'hmac_str': hmac_str,
|
||||
'signature': signature,
|
||||
'auth': auth
|
||||
})
|
||||
|
||||
output_headers['On-Nonce'] = nonce
|
||||
output_headers['Authorization'] = auth
|
||||
|
||||
def _make_headers(self, method, path, query={}, headers={}):
|
||||
'''
|
||||
Creates a headers object to sign the request
|
||||
|
||||
Args:
|
||||
- method (str): HTTP method
|
||||
- path (str): Request path, e.g. /api/documents. No query string
|
||||
- query (dict, default={}): Query string in key-value format
|
||||
- headers (dict, default={}): Other headers to pass in
|
||||
|
||||
Returns:
|
||||
- dict: Dictionary containing all headers
|
||||
'''
|
||||
|
||||
date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||
ctype = headers.get('Content-Type') if headers.get('Content-Type') else 'application/json'
|
||||
|
||||
req_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Date': date,
|
||||
'User-Agent': 'Onshape Python Sample App',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
self._append_auth(req_headers, method, date, path, query=query, ctype=ctype)
|
||||
|
||||
# add in user-defined headers
|
||||
for h in headers:
|
||||
req_headers[h] = headers[h]
|
||||
|
||||
return req_headers
|
||||
|
||||
def request(self, method, path, query={}, headers={}, body={}, base_url=None):
|
||||
'''
|
||||
Issues a request to Onshape
|
||||
|
||||
Args:
|
||||
- method (str): HTTP method
|
||||
- path (str): Path e.g. /api/documents/:id
|
||||
- query (dict, default={}): Query params in key-value pairs
|
||||
- headers (dict, default={}): Key-value pairs of headers
|
||||
- body (dict, default={}): Body for POST request
|
||||
- base_url (str, default=None): Host, including scheme and port (if different from creds file)
|
||||
|
||||
Returns:
|
||||
- requests.Response: Object containing the response from Onshape
|
||||
'''
|
||||
|
||||
req_headers = self._make_headers(method, path, query, headers)
|
||||
if base_url is None:
|
||||
base_url = self._url
|
||||
url = base_url + path + '?' + urllib.parse.urlencode(query)
|
||||
|
||||
if self._logging:
|
||||
utils.log(body)
|
||||
utils.log(req_headers)
|
||||
utils.log('request url: ' + url)
|
||||
|
||||
# only parse as json string if we have to
|
||||
body = json.dumps(body) if type(body) == dict else body
|
||||
|
||||
res = requests.request(method, url, headers=req_headers, data=body, allow_redirects=False, stream=True)
|
||||
|
||||
if res.status_code == 307:
|
||||
location = urlparse(res.headers["Location"])
|
||||
querystring = parse_qs(location.query)
|
||||
|
||||
if self._logging:
|
||||
utils.log('request redirected to: ' + location.geturl())
|
||||
|
||||
new_query = {}
|
||||
new_base_url = location.scheme + '://' + location.netloc
|
||||
|
||||
for key in querystring:
|
||||
new_query[key] = querystring[key][0] # won't work for repeated query params
|
||||
|
||||
return self.request(method, location.path, query=new_query, headers=headers, base_url=new_base_url)
|
||||
elif not 200 <= res.status_code <= 206:
|
||||
print(url)
|
||||
print('! ERROR ('+str(res.status_code)+') while using Onshape API')
|
||||
if res.text:
|
||||
print('! '+res.text)
|
||||
|
||||
if res.status_code == 403:
|
||||
print('HINT: Check that your access rights are correct, and that the clock on your computer is set correctly')
|
||||
exit()
|
||||
if self._logging:
|
||||
utils.log('request failed, details: ' + res.text, level=1)
|
||||
else:
|
||||
if self._logging:
|
||||
utils.log('request succeeded, details: ' + res.text)
|
||||
|
||||
return res
|
||||
Reference in New Issue
Block a user