Constructing API Requests
Sequence of Events
You first need to Generate credentials for API access. Treat the secret key as you would treat a password. You will also use sets of endpoint URLs:
One to get an access token for a particular end user, at
https://www.sidefx.com/oauth2/application_token
One to make API calls, at
https://www.sidefx.com/api/
Retrieve an access token:
Contatenate the three strings: client id,
":"
, and client_secret_key.Base64-encode the contactenated string (name it <auth>).
Pass in an
Authorization
HTTP header with the value set to"Basic <auth>"
where <auth> is the base64-encoded stringMake a POST request using https and not http to the access token endpoint URL.
Parse the returned body as JSON and fetch the access_token field in the result (name it <token>).
If the HTTP status is something other than 200, the body will contain an error message.
Make API requests using the access token. You may reuse the access token over and over to make multiple API requests, but note that access tokens will eventually expire (typically within 12 hours).
Pass in an
Authorization
HTTP header with the value set to"Bearer <token>"
where <token> is the access token returned previously.Construct a JSON array containing three elements:
A string containing the name of the API function you want to call, in the form
module_name.function_name
.A list of values of positional arguments to pass to the function.
A JSON object (dictionary) of keyword arguments to pass to the function.
Pass a
json
parameter in the post data with the value set to the array described above.Make a POST request using https and not http to the API endpoint URL.
Parse the returned body as JSON.
If the HTTP status is something other than 200, the body will contain an error message.
Python Reference Implementation
We provide a Python implementation of what is described in the previous section to easily authenticate to our API with a convenient wrapper to make calls. Feel free to place this code in a file and import it within your script.
# Save this code to sidefx.py
from __future__ import print_function, absolute_import
import time
import json
import base64
import io
import html
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
def service(
client_id, client_secret_key,
access_token_url="https://www.sidefx.com/oauth2/application_token",
endpoint_url="https://www.sidefx.com/api/",
access_token=None, access_token_expiry_time=None, timeout=None):
if (access_token is None or
access_token_expiry_time is None or
access_token_expiry_time < time.time()):
access_token, access_token_expiry_time = (
get_access_token_and_expiry_time(
access_token_url, client_id, client_secret_key,
timeout=timeout))
return _Service(
endpoint_url, access_token, access_token_expiry_time, timeout=timeout)
class _Service(object):
def __init__(
self, endpoint_url, access_token, access_token_expiry_time,
timeout):
self.endpoint_url = endpoint_url
self.access_token = access_token
self.access_token_expiry_time = access_token_expiry_time
self.timeout = timeout
def __getattr__(self, attr_name):
return _APIFunction(attr_name, self)
class _APIFunction(object):
def __init__(self, function_name, service):
self.function_name = function_name
self.service = service
def __getattr__(self, attr_name):
# This isn't actually an API function, but a family of them. Append
# the requested function name to our name.
return _APIFunction(
"%s.%s" % (self.function_name, attr_name), self.service)
def __call__(self, *args, **kwargs):
return call_api_with_access_token(
self.service.endpoint_url, self.service.access_token,
self.function_name, args, kwargs,
timeout=self.service.timeout)
class File(object):
"""Pass parameters of this type to API functions as a way of uploading
large files. Note that these File parameters must be specified by keyword
arguments when calling the functions.
"""
def __init__(self, filename):
self.filename = filename
class ResponseFile(object):
"""This object is returned from API functions that stream binary content.
Call the API function from a `with` statement, and call the read method
on the object to read the data in chunks.
"""
def __init__(self, response):
self.response = response
def __enter__(self):
return self.response.raw
def __exit__(self, exc_type, exc_val, exc_tb):
self.response.close()
#------------------------------------------------------------------------------
# Code that implements authentication and raw calls into the API:
def get_access_token_and_expiry_time(
access_token_url, client_id, client_secret_key, timeout=None):
"""Given an API client (id and secret key) that is allowed to make API
calls, return an access token that can be used to make calls.
"""
# If they're trying to use the /token URL directly then assume this is a
# client-credentials application.
post_data = {}
if (access_token_url.endswith("/token") or
access_token_url.endswith("/token/")):
post_data["grant_type"] = "client_credentials"
response = requests.post(
access_token_url,
headers={
"Authorization": u"Basic {0}".format(
base64.b64encode(
"{0}:{1}".format(
client_id, client_secret_key
).encode()
).decode('utf-8')
),
},
data=post_data,
timeout=timeout)
if response.status_code != 200:
raise AuthorizationError(
response.status_code,
"{0}: {1}".format(
response.status_code,
_extract_traceback_from_response(response)))
response_json = response.json()
access_token_expiry_time = time.time() - 2 + response_json["expires_in"]
return response_json["access_token"], access_token_expiry_time
class AuthorizationError(Exception):
"""Raised from the client if the server generated an error while generating
an access token.
"""
def __init__(self, http_code, message):
super(AuthorizationError, self).__init__(message)
self.http_code = http_code
def call_api_with_access_token(
endpoint_url, access_token, function_name, args, kwargs,
timeout=None):
"""Call into the API using an access token that was returned by
get_access_token.
"""
file_data = {}
for arg_name, arg_value in kwargs.items():
if isinstance(arg_value, (bytearray, File)):
if isinstance(arg_value, File):
file_data[arg_name] = (
arg_value.filename, open(arg_value.filename, "rb"),
"application/octet-stream")
else:
file_data[arg_name] = (
"unnamed.bin", io.BytesIO(arg_value),
"application/octet-stream")
for arg_name in file_data:
del kwargs[arg_name]
post_data = dict(json=json.dumps([function_name, args, kwargs]))
# urllib3 renamed the method_whitelist argument to allowed_methods, so
# handle different versions of urllib3.
retry_kwargs = dict(
total=3,
status_forcelist=[429],
allowed_methods=["GET", "POST"],
backoff_factor=1,
)
try:
retry_strategy = Retry(**retry_kwargs)
except TypeError:
retry_kwargs["method_whitelist"] = retry_kwargs["allowed_methods"]
del retry_kwargs["allowed_methods"]
retry_strategy = Retry(**retry_kwargs)
adapter = HTTPAdapter(max_retries=retry_strategy)
http = requests.Session()
http.mount("https://", adapter)
http.mount("http://", adapter)
response = http.post(
endpoint_url,
headers={"Authorization": "Bearer " + access_token},
data=post_data,
timeout=timeout,
files=file_data,
stream=True)
if response.status_code == 200:
if response.headers.get("Content-Type") == "application/octet-stream":
return ResponseFile(response)
return response.json()
raise APIError(
response.status_code,
_extract_traceback_from_response(response))
class APIError(Exception):
"""Raised from the client if the server generated an error while calling
into the API.
"""
def __init__(self, http_code, message):
super(APIError, self).__init__(message)
self.http_code = http_code
def __str__(self):
return "%s %s" % (self.http_code, self.args[0])
def _extract_traceback_from_response(response):
"""Helper function to extract a traceback from the web server response
if an API call generated a server-side exception and the server is running
in debug mode. In production mode, the server will send back just the
stack trace without the need to parse any html.
"""
error_message = response.text
if response.status_code != 500:
return error_message
traceback = ""
for line in error_message.split("\n"):
if traceback and line == "</textarea>":
break
if line == "Traceback:" or traceback:
traceback += line + "\n"
if traceback:
traceback = error_message
return html.unescape(traceback)
Using the Python library
Assuming the previous code was saved in a sidefx.py
file (or download it
), here is an example on how to call the API. Replace the client_id
and client_secret
values with the ones associated to the Oauth application you previously created (see Generate credentials for API access).
import sidefx
if __name__ == '__main__':
# This service object retrieve a token using your Application ID and secret
service = sidefx.service(
client_id='your OAuth application ID',
client_secret_key='your OAuth application secret')