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,11 @@
|
||||
'''
|
||||
onshape_api, based on onshape "apikey"
|
||||
======
|
||||
|
||||
Demonstrates usage of API keys for the Onshape REST API
|
||||
'''
|
||||
|
||||
__copyright__ = 'Copyright (c) 2016 Onshape, Inc.'
|
||||
__license__ = 'All rights reserved.'
|
||||
__title__ = 'onshape_api'
|
||||
__all__ = ['onshape', 'client', 'utils']
|
||||
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
import inspect
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import pickle
|
||||
|
||||
|
||||
def get_cache_path() -> Path:
|
||||
"""
|
||||
Return the path to the user cache.
|
||||
"""
|
||||
path = Path.home() / ".cache" / "onshape-to-robot"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def can_cache(method, *args, **kwargs) -> bool:
|
||||
"""
|
||||
Check if the cache can be used.
|
||||
When using wmv=w, the current workspace is used, which make it impossible to cache.
|
||||
"""
|
||||
signature = inspect.signature(method)
|
||||
wmv = None
|
||||
if "wmv" in signature.parameters:
|
||||
wmv = signature.parameters["wmv"].default
|
||||
if "wmv" in kwargs:
|
||||
wmv = kwargs["wmv"]
|
||||
return wmv != "w"
|
||||
|
||||
|
||||
def cache_response(method):
|
||||
"""
|
||||
Decorator that caches the response of a method.
|
||||
"""
|
||||
|
||||
def cached_call(*args, **kwargs):
|
||||
# Checking if the method can be cached
|
||||
if not can_cache(method, *args, **kwargs):
|
||||
return method(*args, **kwargs)
|
||||
|
||||
# Building filename that is unique for method/args combination
|
||||
method_name = method.__qualname__
|
||||
arguments = {"args": args, "kwargs": kwargs}
|
||||
arguments_hash = hashlib.sha1(pickle.dumps(arguments)).hexdigest()
|
||||
filename = f"{get_cache_path()}/{method_name}_{arguments_hash}.pkl"
|
||||
|
||||
if not os.path.exists(filename):
|
||||
result = method(*args, **kwargs)
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(result, f)
|
||||
|
||||
return pickle.load(open(filename, "rb"))
|
||||
|
||||
return cached_call
|
||||
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
client
|
||||
======
|
||||
|
||||
Convenience functions for working with the Onshape API
|
||||
"""
|
||||
|
||||
from .onshape import Onshape
|
||||
from .cache import cache_response
|
||||
|
||||
|
||||
def escape(s):
|
||||
return s.replace("/", "%2f").replace("+", "%2b")
|
||||
|
||||
|
||||
class Client:
|
||||
"""
|
||||
Defines methods for testing the Onshape API. Comes with several methods:
|
||||
|
||||
- Create a document
|
||||
- Delete a document
|
||||
- Get a list of documents
|
||||
|
||||
Attributes:
|
||||
- stack (str, default='https://cad.onshape.com'): Base URL
|
||||
- logging (bool, default=True): Turn logging on or off
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, stack="https://cad.onshape.com", logging=True, creds="./config.json"
|
||||
):
|
||||
"""
|
||||
Instantiates a new Onshape client.
|
||||
|
||||
Args:
|
||||
- stack (str, default='https://cad.onshape.com'): Base URL
|
||||
- logging (bool, default=True): Turn logging on or off
|
||||
"""
|
||||
|
||||
self._metadata_cache = {}
|
||||
self._massproperties_cache = {}
|
||||
self._stack = stack
|
||||
self._api = Onshape(stack=stack, logging=logging, creds=creds)
|
||||
|
||||
def request(self, url, **kwargs):
|
||||
return self._api.request("get", url, **kwargs).json()
|
||||
|
||||
def request_binary(self, url, **kwargs):
|
||||
return self._api.request("get", url, **kwargs).content
|
||||
|
||||
@cache_response
|
||||
def get_document(self, did):
|
||||
"""
|
||||
Get details for a specified document.
|
||||
|
||||
Args:
|
||||
- did (str): Document ID
|
||||
|
||||
Returns:
|
||||
- requests.Response: Onshape response data
|
||||
"""
|
||||
return self.request(f"/api/documents/{escape(did)}")
|
||||
|
||||
@cache_response
|
||||
def list_elements(self, did, wid, wmv="w"):
|
||||
"""
|
||||
Get the list of elements in a given document
|
||||
"""
|
||||
|
||||
return self.request(
|
||||
f"/api/documents/d/{escape(did)}/{escape(wmv)}/{escape(wid)}/elements"
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def get_assembly(self, did, wmvid, eid, wmv="w", configuration="default"):
|
||||
"""
|
||||
Retrieve the assembly structure for a specified document / workspace / element.
|
||||
"""
|
||||
return self.request(
|
||||
f"/api/assemblies/d/{escape(did)}/{escape(wmv)}/{escape(wmvid)}/e/{escape(eid)}",
|
||||
query={
|
||||
"includeMateFeatures": "true",
|
||||
"includeMateConnectors": "true",
|
||||
"includeNonSolids": "true",
|
||||
"configuration": configuration,
|
||||
},
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def get_features(self, did, wvid, eid, wmv="w", configuration="default"):
|
||||
"""
|
||||
Gets the feature list for specified document / workspace / part studio.
|
||||
|
||||
Args:
|
||||
- did (str): Document ID
|
||||
- mid (str): Microversion
|
||||
- eid (str): Element ID
|
||||
|
||||
Returns:
|
||||
- requests.Response: Onshape response data
|
||||
"""
|
||||
|
||||
return self.request(
|
||||
f"/api/assemblies/d/{escape(did)}/{escape(wmv)}/{escape(wvid)}/e/{escape(eid)}/features",
|
||||
query={"configuration": configuration},
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def get_sketches(self, did, mid, eid, configuration):
|
||||
"""
|
||||
Get sketches for a given document / microversion / element.
|
||||
"""
|
||||
return self.request(
|
||||
f"/api/partstudios/d/{escape(did)}/m/{escape(mid)}/e/{escape(eid)}/sketches",
|
||||
query={"includeGeometry": "true", "configuration": configuration},
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def get_parts(self, did, mid, eid, configuration):
|
||||
"""
|
||||
Get parts for a given document / microversion / element.
|
||||
"""
|
||||
return self.request(
|
||||
f"/api/parts/d/{escape(did)}/m/{escape(mid)}/e/{escape(eid)}",
|
||||
query={"configuration": configuration},
|
||||
)
|
||||
|
||||
def find_new_partid(
|
||||
self, did, mid, eid, partid, configuration_before, configuration
|
||||
):
|
||||
before = self.get_parts(did, mid, eid, configuration_before)
|
||||
name = None
|
||||
for entry in before:
|
||||
if entry["partId"] == partid:
|
||||
name = entry["name"]
|
||||
|
||||
if name is not None:
|
||||
after = self.get_parts(did, mid, eid, configuration)
|
||||
for entry in after:
|
||||
if entry["name"] == name:
|
||||
return entry["partId"]
|
||||
else:
|
||||
print("Onshape ERROR: Can't find new partid for " + str(partid))
|
||||
|
||||
return partid
|
||||
|
||||
@cache_response
|
||||
def part_studio_stl_m(
|
||||
self,
|
||||
did,
|
||||
wmvid,
|
||||
eid,
|
||||
partid="",
|
||||
wmv="m",
|
||||
configuration="default",
|
||||
linked_document_id=None,
|
||||
):
|
||||
req_headers = {"Accept": "*/*"}
|
||||
query = {
|
||||
"mode": "binary",
|
||||
"units": "meter",
|
||||
"configuration": configuration,
|
||||
}
|
||||
if linked_document_id is not None:
|
||||
query["linkDocumentId"] = linked_document_id
|
||||
return self.request_binary(
|
||||
f"/api/parts/d/{escape(did)}/{escape(wmv)}/{escape(wmvid)}/e/{escape(eid)}/partid/{escape(partid)}/stl",
|
||||
query=query,
|
||||
headers=req_headers,
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def matevalues(self, did, wmvid, eid, wmv="w", configuration="default"):
|
||||
return self.request(
|
||||
f"/api/assemblies/d/{escape(did)}/{wmv}/{escape(wmvid)}/e/{escape(eid)}/matevalues",
|
||||
query={"configuration": configuration},
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def part_get_metadata(
|
||||
self,
|
||||
did,
|
||||
wmvid,
|
||||
eid,
|
||||
partid,
|
||||
wmv="m",
|
||||
configuration="default",
|
||||
linked_document_id=None,
|
||||
):
|
||||
query = {"configuration": configuration}
|
||||
if linked_document_id is not None:
|
||||
query["linkDocumentId"] = linked_document_id
|
||||
return self.request(
|
||||
f"/api/metadata/d/{escape(did)}/{escape(wmv)}/{escape(wmvid)}/e/{escape(eid)}/p/{escape(partid)}",
|
||||
query=query,
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def part_mass_properties(
|
||||
self,
|
||||
did,
|
||||
wmvid,
|
||||
eid,
|
||||
partid,
|
||||
wmv="m",
|
||||
configuration="default",
|
||||
linked_document_id=None,
|
||||
):
|
||||
query = {
|
||||
"configuration": configuration,
|
||||
"useMassPropertyOverrides": True,
|
||||
}
|
||||
if linked_document_id is not None:
|
||||
query["linkDocumentId"] = linked_document_id
|
||||
return self.request(
|
||||
f"/api/parts/d/{escape(did)}/{escape(wmv)}/{escape(wmvid)}/e/{escape(eid)}/partid/{escape(partid)}/massproperties",
|
||||
query=query,
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def standard_cont_mass_properties(
|
||||
self, did, vid, eid, partid, linked_document_id, configuration
|
||||
):
|
||||
return self.request(
|
||||
f"/api/parts/d/{escape(did)}/v/{escape(vid)}/e/{escape(eid)}/partid/{escape(partid)}/massproperties",
|
||||
query={
|
||||
"configuration": configuration,
|
||||
"useMassPropertyOverrides": True,
|
||||
"linkDocumentId": linked_document_id,
|
||||
"inferMetadataOwner": True,
|
||||
},
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def elements_configuration(
|
||||
self, did, wmvid, eid, wmv, linked_document_id=None, configuration=None
|
||||
):
|
||||
query = {}
|
||||
if linked_document_id is not None:
|
||||
query["linkDocumentId"] = linked_document_id
|
||||
return self.request(
|
||||
f"/api/elements/d/{escape(did)}/{escape(wmv)}/{escape(wmvid)}/e/{escape(eid)}/configuration",
|
||||
query=query,
|
||||
)
|
||||
|
||||
@cache_response
|
||||
def get_variables(self, did, wvid, eid, wmv, configuration):
|
||||
return self.request(
|
||||
f"/api/variables/d/{escape(did)}/{escape(wmv)}/{escape(wvid)}/e/{escape(eid)}/variables",
|
||||
query={
|
||||
"configuration": configuration,
|
||||
"includeValuesAndReferencedVariables": True,
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,74 @@
|
||||
'''
|
||||
utils
|
||||
=====
|
||||
|
||||
Handy functions for API key sample app
|
||||
'''
|
||||
|
||||
import logging
|
||||
from logging.config import dictConfig
|
||||
|
||||
__all__ = [
|
||||
'log'
|
||||
]
|
||||
|
||||
|
||||
def log(msg, level=0):
|
||||
'''
|
||||
Logs a message to the console, with optional level paramater
|
||||
|
||||
Args:
|
||||
- msg (str): message to send to console
|
||||
- level (int): log level; 0 for info, 1 for error (default = 0)
|
||||
'''
|
||||
|
||||
red = '\033[91m'
|
||||
endc = '\033[0m'
|
||||
|
||||
# configure the logging module
|
||||
cfg = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'stdout': {
|
||||
'format': '[%(levelname)s]: %(asctime)s - %(message)s',
|
||||
'datefmt': '%x %X'
|
||||
},
|
||||
'stderr': {
|
||||
'format': red + '[%(levelname)s]: %(asctime)s - %(message)s' + endc,
|
||||
'datefmt': '%x %X'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'stdout': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'level': 'DEBUG',
|
||||
'formatter': 'stdout'
|
||||
},
|
||||
'stderr': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'level': 'ERROR',
|
||||
'formatter': 'stderr'
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'info': {
|
||||
'handlers': ['stdout'],
|
||||
'level': 'INFO',
|
||||
'propagate': True
|
||||
},
|
||||
'error': {
|
||||
'handlers': ['stderr'],
|
||||
'level': 'ERROR',
|
||||
'propagate': False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dictConfig(cfg)
|
||||
|
||||
lg = 'info' if level == 0 else 'error'
|
||||
lvl = 20 if level == 0 else 40
|
||||
|
||||
logger = logging.getLogger(lg)
|
||||
logger.log(lvl, msg)
|
||||
Reference in New Issue
Block a user