221 lines
7.4 KiB
Python
221 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Copyright Contributors to the OpenVDB Project
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Python script to download the latest Houdini builds
|
|
# using the SideFX download API:
|
|
#
|
|
# https://www.sidefx.com/docs/api/download/index.html
|
|
#
|
|
# Authors: Dan Bailey, SideFX
|
|
|
|
import time
|
|
import sys
|
|
import re
|
|
import shutil
|
|
import json
|
|
import base64
|
|
import requests
|
|
import hashlib
|
|
import os
|
|
import argparse
|
|
import copy
|
|
|
|
# For progress bar printing
|
|
try:
|
|
from tqdm import tqdm
|
|
has_tqdm = True
|
|
except:
|
|
has_tqdm = False
|
|
pass
|
|
|
|
# Code that provides convenient Python wrappers to call into the API:
|
|
|
|
def service(
|
|
access_token_url, client_id, client_secret_key, endpoint_url,
|
|
access_token=None, access_token_expiry_time=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))
|
|
|
|
return _Service(
|
|
endpoint_url, access_token, access_token_expiry_time)
|
|
|
|
|
|
class _Service(object):
|
|
def __init__(
|
|
self, endpoint_url, access_token, access_token_expiry_time):
|
|
self.endpoint_url = endpoint_url
|
|
self.access_token = access_token
|
|
self.access_token_expiry_time = access_token_expiry_time
|
|
|
|
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(
|
|
"{0}.{1}".format(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)
|
|
|
|
#---------------------------------------------------------------------------
|
|
# 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):
|
|
"""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.
|
|
"""
|
|
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')
|
|
),
|
|
})
|
|
if response.status_code != 200:
|
|
raise AuthorizationError(response.status_code, response.text)
|
|
|
|
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):
|
|
"""Call into the API using an access token that was returned by
|
|
get_access_token.
|
|
"""
|
|
response = requests.post(
|
|
endpoint_url,
|
|
headers={
|
|
"Authorization": "Bearer " + access_token,
|
|
},
|
|
data=dict(
|
|
json=json.dumps([function_name, args, kwargs]),
|
|
))
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
|
|
raise APIError(response.status_code, response.text)
|
|
|
|
|
|
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
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
parser = argparse.ArgumentParser(description='Download a Houdini Installation')
|
|
parser.add_argument('version', type=str, help='Major.Minor version of Houdini to download')
|
|
parser.add_argument('platform', type=str, help='Platform target')
|
|
parser.add_argument('--prod', action='store_true', help='Only download production builds')
|
|
parser.add_argument('--list', action='store_true', help='Just list the available builds and exit.')
|
|
args = parser.parse_args()
|
|
|
|
version = args.version
|
|
platform = args.platform
|
|
only_production = args.prod
|
|
|
|
user_client_id = os.getenv('HOUDINI_CLIENT_ID')
|
|
user_client_secret_key = os.getenv('HOUDINI_SECRET_KEY')
|
|
|
|
if not re.match('[0-9][0-9]\.[0-9]$', version):
|
|
raise IOError('Invalid Houdini Version "%s", expecting in the form "major.minor" such as "16.0"' % version)
|
|
|
|
service = service(
|
|
access_token_url="https://www.sidefx.com/oauth2/application_token",
|
|
client_id=user_client_id,
|
|
client_secret_key=user_client_secret_key,
|
|
endpoint_url="https://www.sidefx.com/api/",
|
|
)
|
|
|
|
releases_list = service.download.get_daily_builds_list(
|
|
product='houdini', version=version, platform=platform, only_production=only_production)
|
|
|
|
print('Available builds:')
|
|
for rel in releases_list:
|
|
rel = copy.deepcopy(rel)
|
|
if 'third_party_libraries' in rel:
|
|
# Don't print these
|
|
del rel['third_party_libraries']
|
|
print(rel)
|
|
|
|
if args.list:
|
|
sys.exit(0)
|
|
|
|
print('Selecting build: ' + releases_list[0]['build'])
|
|
|
|
latest_release = service.download.get_daily_build_download(
|
|
product='houdini', version=version, platform=platform, build=releases_list[0]['build'])
|
|
print(latest_release)
|
|
|
|
# Can't do this procedurally as latest_release['filename'] can contain
|
|
# multiple periods and may have multiple trailing extensions...
|
|
extension = ''
|
|
if 'linux' in platform: extension = 'tar.gz'
|
|
elif 'macos' in platform: extension = 'dmg'
|
|
elif 'win64' in platform: extension = 'exe'
|
|
assert(extension in latest_release['filename'])
|
|
|
|
# Download the file and save it as hou.extension
|
|
local_filename = 'hou.' + extension
|
|
print('Writing to "' + local_filename + '"')
|
|
|
|
response = requests.get(latest_release['download_url'], stream=True)
|
|
if response.status_code == 200:
|
|
response.raw.decode_content = True
|
|
if has_tqdm:
|
|
file_size = int(response.headers.get('Content-Length', 0))
|
|
desc = "(Unknown total file size)" if file_size == 0 else ""
|
|
with tqdm.wrapattr(response.raw, "read", total=file_size, desc=desc) as r_raw:
|
|
with open(local_filename, 'wb') as f:
|
|
shutil.copyfileobj(r_raw, f)
|
|
else:
|
|
with open(local_filename, 'wb') as f:
|
|
shutil.copyfileobj(response.raw, f)
|
|
else:
|
|
raise Exception('Error downloading file!')
|
|
|
|
# Verify the file checksum is matching
|
|
file_hash = hashlib.md5()
|
|
with open(local_filename, 'rb') as f:
|
|
for chunk in iter(lambda: f.read(4096), b''):
|
|
file_hash.update(chunk)
|
|
if file_hash.hexdigest() != latest_release['hash']:
|
|
raise Exception('Checksum does not match!')
|