#!/usr/bin/env python
"""Sauce Labs REST API client
Copyright (c) 2013-2017 Corey Goldberg
This file is part of: sauceclient
https://github.com/cgoldberg/sauceclient
License: Apache Version 2.0
http://www.apache.org/licenses/LICENSE-2.0
Sauce Labs REST API documentation:
http://saucelabs.com/docs/rest
"""
import base64
import hmac
import json
import os
from hashlib import md5
try:
import http.client as http_client
from urllib.parse import urlencode
except ImportError:
import httplib as http_client
from urllib import urlencode
__version__ = '1.0.1'
[docs]class SauceException(Exception):
"""SauceClient exception."""
def __init__(self, *args, **kwargs):
"""Initialize class."""
super(SauceException, self).__init__(*args)
self.response = kwargs.get('response')
[docs]class SauceClient(object):
"""SauceClient class."""
def __init__(self, sauce_username=None, sauce_access_key=None, apibase=None):
"""Initialize class."""
self.sauce_username = sauce_username
self.sauce_access_key = sauce_access_key
self.apibase = apibase or 'saucelabs.com'
self.headers = self.make_headers()
self.account = Account(self)
self.information = Information(self)
self.javascript = JavaScriptTests(self)
self.jobs = Jobs(self)
self.storage = Storage(self)
self.tunnels = Tunnels(self)
self.analytics = Analytics(self)
[docs] def get_auth_string(self):
"""Create auth string from credentials."""
auth_info = '{}:{}'.format(self.sauce_username, self.sauce_access_key)
return base64.b64encode(auth_info.encode('utf-8')).decode('utf-8')
[docs] def request(self, method, url, body=None, content_type='application/json'):
"""Send http request."""
headers = self.make_auth_headers(content_type)
connection = http_client.HTTPSConnection(self.apibase)
connection.request(method, url, body, headers=headers)
response = connection.getresponse()
data = response.read()
connection.close()
if response.status not in [200, 201]:
raise SauceException('{}: {}.\nSauce Status NOT OK'.format(
response.status, response.reason), response=response)
return json.loads(data.decode('utf-8'))
[docs]class Account(object):
"""Account Methods
These methods provide user account information and management.
- https://wiki.saucelabs.com/display/DOCS/Account+Methods
"""
def __init__(self, client):
"""Initialize class."""
self.client = client
[docs] def get_user(self):
"""Access basic account information."""
method = 'GET'
endpoint = '/rest/v1/users/{}'.format(self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def create_user(self, username, password, name, email):
"""Create a sub account."""
method = 'POST'
endpoint = '/rest/v1/users/{}'.format(self.client.sauce_username)
body = json.dumps({'username': username, 'password': password,
'name': name, 'email': email, })
return self.client.request(method, endpoint, body)
[docs] def get_concurrency(self):
"""Check account concurrency limits."""
method = 'GET'
endpoint = '/rest/v1.1/users/{}/concurrency'.format(
self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def get_subaccounts(self):
"""Get a list of sub accounts associated with a parent account."""
method = 'GET'
endpoint = '/rest/v1/users/{}/list-subaccounts'.format(
self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def get_siblings(self):
"""Get a list of sibling accounts associated with provided account."""
method = 'GET'
endpoint = '/rest/v1.1/users/{}/siblings'.format(
self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def get_subaccount_info(self):
"""Get information about a sub account."""
method = 'GET'
endpoint = '/rest/v1/users/{}/subaccounts'.format(
self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def change_access_key(self):
"""Change access key of your account."""
method = 'POST'
endpoint = '/rest/v1/users/{}/accesskey/change'.format(
self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def get_activity(self):
"""Check account concurrency limits."""
method = 'GET'
endpoint = '/rest/v1/{}/activity'.format(self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def get_usage(self, start=None, end=None):
"""Access historical account usage data."""
method = 'GET'
endpoint = '/rest/v1/users/{}/usage'.format(self.client.sauce_username)
data = {}
if start:
data['start'] = start
if end:
data['end'] = end
if data:
endpoint = '?'.join([endpoint, urlencode(data)])
return self.client.request(method, endpoint)
[docs]class Analytics(object):
"""Analytics Methods
These methods provide user account information and management.
- https://wiki.saucelabs.com/display/DOCS/Analytics+Methods
"""
def __init__(self, client):
self.client = client
[docs] def get_test_trends(self, start=None, end=None, interval=None, time_range=None, scope=None,
owner=None, status=None, pretty=False, os=None,
browser=None):
method = 'GET'
endpoint = '/rest/v1/analytics/trends/tests'
data = {}
if time_range:
data['time_range'] = time_range
if start:
data['start'] = start
if end:
data['end'] = end
if interval:
data['interval'] = interval
if scope:
data['scope'] = scope
if owner:
data['owner'] = owner
if status:
data['status'] = status
if pretty:
data['pretty'] = ''
if os:
data['os'] = os
if browser:
data['browser'] = browser
endpoint = '?'.join([endpoint, urlencode(data)])
return self.client.request(method, endpoint)
[docs] def get_error_trends(self, start=None, end=None, time_range=None, scope=None,
owner=None, status=None, pretty=False, os=None,
browser=None):
method = 'GET'
endpoint = '/rest/v1/analytics/trends/errors'
data = {}
if time_range:
data['time_range'] = time_range
if start:
data['start'] = start
if end:
data['end'] = end
if scope:
data['scope'] = scope
if owner:
data['owner'] = owner
if status:
data['status'] = status
if pretty:
data['pretty'] = ''
if os:
data['os'] = os
if browser:
data['browser'] = browser
endpoint = '?'.join([endpoint, urlencode(data)])
return self.client.request(method, endpoint)
[docs] def get_build_trends(self, start=None, end=None, time_range=None, scope=None,
owner=None, status=None, pretty=False, os=None,
browser=None):
method = 'GET'
endpoint = '/rest/v1/analytics/trends/builds_tests'
data = {}
if time_range:
data['time_range'] = time_range
if start:
data['start'] = start
if end:
data['end'] = end
if scope:
data['scope'] = scope
if owner:
data['owner'] = owner
if status:
data['status'] = status
if pretty:
data['pretty'] = ''
if os:
data['os'] = os
if browser:
data['browser'] = browser
endpoint = '?'.join([endpoint, urlencode(data)])
return self.client.request(method, endpoint)
[docs] def get_tests(self, start=None, end=None, size=None, time_range=None, scope=None,
owner=None, status=None, pretty=False, error=None, build=None,
skip=None,missing_build=False):
method = 'GET'
endpoint = '/rest/v1/analytics/tests'
data = {}
if time_range:
data['time_range'] = time_range
if start:
data['start'] = start
if end:
data['end'] = end
if size:
data['size'] = size
if scope:
data['scope'] = scope
if owner:
data['owner'] = owner
if status:
data['status'] = status
if pretty:
data['pretty'] = ''
if error:
data['error'] = error
if build:
data['build'] = build
#from is a reserved keyword, using skip instead
if skip:
data['from'] = skip
if missing_build:
data['missing_build'] = ''
endpoint = '?'.join([endpoint, urlencode(data)])
return self.client.request(method, endpoint)
[docs] def get_concurrency(self, start=None, end=None, interval=None, time_range=None, scope=None,
owner=None, status=None, pretty=False):
method = 'GET'
endpoint = '/rest/v1/analytics/insights/concurrency'
data={}
if time_range:
data['time_range'] = time_range
if start:
data['start'] = start
if end:
data['end'] = end
if interval:
data['interval'] = interval
if scope:
data['scope'] = scope
if owner:
data['owner'] = owner
if status:
data['status'] = status
if pretty:
data['pretty'] = ''
endpoint = '?'.join([endpoint, urlencode(data)])
return self.client.request(method, endpoint)
[docs]class JavaScriptTests(object):
"""JavaScript Unit Testing Methods
- https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods
"""
def __init__(self, client):
self.client = client
[docs] def js_tests(self, platforms, url, framework):
"""Start your JavaScript unit tests on as many browsers as you like
with a single request."""
method = 'POST'
endpoint = '/rest/v1/{}/js-tests'.format(self.client.sauce_username)
body = json.dumps({'platforms': platforms, 'url': url,
'framework': framework, })
return self.client.request(method, endpoint, body)
[docs] def js_tests_status(self, js_tests):
"""Get the status of your JS unit tests."""
method = 'POST'
endpoint = '/rest/v1/{}/js-tests/status'.format(
self.client.sauce_username)
body = json.dumps({
'js tests': js_tests,
})
return self.client.request(method, endpoint, body)
[docs]class Jobs(object):
"""Job Methods
- https://wiki.saucelabs.com/display/DOCS/Job+Methods
"""
def __init__(self, client):
"""Initialize class."""
self.client = client
[docs] def get_jobs(self, full=None, limit=None, skip=None, start=None, end=None,
output_format=None):
"""List jobs belonging to a specific user."""
method = 'GET'
endpoint = '/rest/v1/{}/jobs'.format(self.client.sauce_username)
data = {}
if full is not None:
data['full'] = full
if limit is not None:
data['limit'] = limit
if skip is not None:
data['skip'] = skip
if start is not None:
data['from'] = start
if end is not None:
data['to'] = end
if output_format is not None:
data['format'] = output_format
if data:
endpoint = '?'.join([endpoint, urlencode(data)])
return self.client.request(method, endpoint)
[docs] def get_job(self, job_id):
"""Retreive a single job."""
method = 'GET'
endpoint = '/rest/v1/{}/jobs/{}'.format(self.client.sauce_username,
job_id)
return self.client.request(method, endpoint)
[docs] def update_job(self, job_id, build=None, custom_data=None,
name=None, passed=None, public=None, tags=None):
"""Edit an existing job."""
method = 'PUT'
endpoint = '/rest/v1/{}/jobs/{}'.format(self.client.sauce_username,
job_id)
data = {}
if build is not None:
data['build'] = build
if custom_data is not None:
data['custom-data'] = custom_data
if name is not None:
data['name'] = name
if passed is not None:
data['passed'] = passed
if public is not None:
data['public'] = public
if tags is not None:
data['tags'] = tags
body = json.dumps(data)
return self.client.request(method, endpoint, body=body)
[docs] def delete_job(self, job_id):
"""Removes the job from the system with all the linked assets."""
method = 'DELETE'
endpoint = '/rest/v1/{}/jobs/{}'.format(self.client.sauce_username,
job_id)
return self.client.request(method, endpoint)
[docs] def stop_job(self, job_id):
"""Terminates a running job."""
method = 'PUT'
endpoint = '/rest/v1/{}/jobs/{}/stop'.format(
self.client.sauce_username, job_id)
return self.client.request(method, endpoint)
[docs] def get_job_assets(self, job_id):
"""Get details about the static assets collected for a specific job."""
method = 'GET'
endpoint = '/rest/v1/{}/jobs/{}/assets'.format(
self.client.sauce_username, job_id)
return self.client.request(method, endpoint)
[docs] def get_job_asset_url(self, job_id, filename):
"""Get details about the static assets collected for a specific job."""
return 'https://saucelabs.com/rest/v1/{}/jobs/{}/assets/{}'.format(
self.client.sauce_username, job_id, filename)
[docs] def delete_job_assets(self, job_id):
"""Delete all the assets captured during a test run."""
method = 'DELETE'
endpoint = '/rest/v1/{}/jobs/{}/assets'.format(
self.client.sauce_username, job_id)
return self.client.request(method, endpoint)
[docs] def get_auth_token(self, job_id, date_range=None):
"""Get an auth token to access protected job resources.
https://wiki.saucelabs.com/display/DOCS/Building+Links+to+Test+Results
"""
key = '{}:{}'.format(self.client.sauce_username,
self.client.sauce_access_key)
if date_range:
key = '{}:{}'.format(key, date_range)
return hmac.new(key.encode('utf-8'), job_id.encode('utf-8'),
md5).hexdigest()
[docs]class Storage(object):
"""Temporary Storage Methods
- https://wiki.saucelabs.com/display/DOCS/Temporary+Storage+Methods
"""
def __init__(self, client):
"""Initialize class."""
self.client = client
[docs] def upload_file(self, filepath, overwrite=True):
"""Uploads a file to the temporary sauce storage."""
method = 'POST'
filename = os.path.split(filepath)[1]
endpoint = '/rest/v1/storage/{}/{}?overwrite={}'.format(
self.client.sauce_username, filename, "true" if overwrite else "false")
with open(filepath, 'rb') as filehandle:
body = filehandle.read()
return self.client.request(method, endpoint, body,
content_type='application/octet-stream')
[docs] def get_stored_files(self):
"""Check which files are in your temporary storage."""
method = 'GET'
endpoint = '/rest/v1/storage/{}'.format(self.client.sauce_username)
return self.client.request(method, endpoint)
[docs]class Tunnels(object):
"""Tunnel Methods
- https://wiki.saucelabs.com/display/DOCS/Tunnel+Methods
"""
def __init__(self, client):
"""Initialize class."""
self.client = client
[docs] def get_tunnels(self):
"""Retrieves all running tunnels for a specific user."""
method = 'GET'
endpoint = '/rest/v1/{}/tunnels'.format(self.client.sauce_username)
return self.client.request(method, endpoint)
[docs] def get_tunnel(self, tunnel_id):
"""Get information for a tunnel given its ID."""
method = 'GET'
endpoint = '/rest/v1/{}/tunnels/{}'.format(
self.client.sauce_username, tunnel_id)
return self.client.request(method, endpoint)
[docs] def delete_tunnel(self, tunnel_id):
"""Get information for a tunnel given its ID."""
method = 'DELETE'
endpoint = '/rest/v1/{}/tunnels/{}'.format(
self.client.sauce_username, tunnel_id)
return self.client.request(method, endpoint)