Bug 1508381 - vendor newest taskcluster client r=tomprince
authorDustin J. Mitchell <dustin@mozilla.com>
Tue, 12 Mar 2019 20:40:04 +0000
changeset 521725 81c183207343ab527f9ce64a6e9637f108e415e6
parent 521724 e212dcda9cce85ee547998dbd119cd87625822a3
child 521726 a0881eefea64df074cb879606f89be7d92bd3fd5
push id10867
push userdvarga@mozilla.com
push dateThu, 14 Mar 2019 15:20:45 +0000
treeherdermozilla-beta@abad13547875 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstomprince
bugs1508381
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1508381 - vendor newest taskcluster client r=tomprince Differential Revision: https://phabricator.services.mozilla.com/D18025
third_party/python/requirements.in
third_party/python/requirements.txt
third_party/python/taskcluster/PKG-INFO
third_party/python/taskcluster/README.md
third_party/python/taskcluster/setup.py
third_party/python/taskcluster/taskcluster/_client_importer.py
third_party/python/taskcluster/taskcluster/aio/_client_importer.py
third_party/python/taskcluster/taskcluster/aio/asyncclient.py
third_party/python/taskcluster/taskcluster/aio/asyncutils.py
third_party/python/taskcluster/taskcluster/aio/auth.py
third_party/python/taskcluster/taskcluster/aio/authevents.py
third_party/python/taskcluster/taskcluster/aio/awsprovisioner.py
third_party/python/taskcluster/taskcluster/aio/awsprovisionerevents.py
third_party/python/taskcluster/taskcluster/aio/ec2manager.py
third_party/python/taskcluster/taskcluster/aio/github.py
third_party/python/taskcluster/taskcluster/aio/githubevents.py
third_party/python/taskcluster/taskcluster/aio/hooks.py
third_party/python/taskcluster/taskcluster/aio/index.py
third_party/python/taskcluster/taskcluster/aio/login.py
third_party/python/taskcluster/taskcluster/aio/notify.py
third_party/python/taskcluster/taskcluster/aio/pulse.py
third_party/python/taskcluster/taskcluster/aio/purgecache.py
third_party/python/taskcluster/taskcluster/aio/purgecacheevents.py
third_party/python/taskcluster/taskcluster/aio/queue.py
third_party/python/taskcluster/taskcluster/aio/queueevents.py
third_party/python/taskcluster/taskcluster/aio/secrets.py
third_party/python/taskcluster/taskcluster/aio/treeherderevents.py
third_party/python/taskcluster/taskcluster/auth.py
third_party/python/taskcluster/taskcluster/authevents.py
third_party/python/taskcluster/taskcluster/awsprovisioner.py
third_party/python/taskcluster/taskcluster/awsprovisionerevents.py
third_party/python/taskcluster/taskcluster/client.py
third_party/python/taskcluster/taskcluster/ec2manager.py
third_party/python/taskcluster/taskcluster/github.py
third_party/python/taskcluster/taskcluster/githubevents.py
third_party/python/taskcluster/taskcluster/hooks.py
third_party/python/taskcluster/taskcluster/index.py
third_party/python/taskcluster/taskcluster/login.py
third_party/python/taskcluster/taskcluster/notify.py
third_party/python/taskcluster/taskcluster/pulse.py
third_party/python/taskcluster/taskcluster/purgecache.py
third_party/python/taskcluster/taskcluster/purgecacheevents.py
third_party/python/taskcluster/taskcluster/queue.py
third_party/python/taskcluster/taskcluster/queueevents.py
third_party/python/taskcluster/taskcluster/secrets.py
third_party/python/taskcluster/taskcluster/treeherderevents.py
third_party/python/taskcluster/taskcluster/utils.py
third_party/python/taskcluster/test/test_async.py
third_party/python/taskcluster/test/test_client.py
third_party/python/taskcluster/test/test_utils.py
--- a/third_party/python/requirements.in
+++ b/third_party/python/requirements.in
@@ -7,12 +7,12 @@ pathlib2==2.3.2
 pip-tools==3.0.0
 pipenv==2018.5.18
 psutil==5.4.3
 pytest==3.6.2
 python-hglib==2.4
 redo==2.0.3
 requests==2.9.1
 six==1.10.0
-taskcluster==4.0.1
+taskcluster==6.0.0
 taskcluster-urls==11.0.0
 virtualenv==15.2.0
 voluptuous==0.11.5
--- a/third_party/python/requirements.txt
+++ b/third_party/python/requirements.txt
@@ -99,20 +99,20 @@ six==1.10.0 \
     --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
 slugid==1.0.7 \
     --hash=sha256:6dab3c7eef0bb423fb54cb7752e0f466ddd0ee495b78b763be60e8a27f69e779 \
     # via taskcluster
 taskcluster-urls==11.0.0 \
     --hash=sha256:18dcaa9c2412d34ff6c78faca33f0dd8f2384e3f00a98d5832c62d6d664741f0 \
     --hash=sha256:2aceab7cf5b1948bc197f2e5e50c371aa48181ccd490b8bada00f1e3baf0c5cc \
     --hash=sha256:74bd2110b5daaebcec5e1d287bf137b61cb8cf6b2d8f5f2b74183e32bc4e7c87
-taskcluster==4.0.1 \
-    --hash=sha256:27256511044346ac71a495d3c636f2add95c102b9b09f90d6fb1ea3e9949d311 \
-    --hash=sha256:99dd90bc1c566968868c8b07ede32f8e031cbccd52c7195a61e802679d461447 \
-    --hash=sha256:d0360063c1a3fcaaa514bb31c03954ba573d2b671df40a2ecfdfd9339cc8e93e
+taskcluster==6.0.0 \
+    --hash=sha256:48ecd4898c7928deddfb34cb1cfe2b2505c68416e6c503f8a7f3dd0572425e96 \
+    --hash=sha256:6d5cf7bdbc09dc48b2d376b418b95c1c157a2d359c4b6b231c1fb14a323c0cc5 \
+    --hash=sha256:e409fce7a72808e4f87dc7baca7a79d8b64d5c5045264b9e197c120cc40e219b
 virtualenv-clone==0.3.0 \
     --hash=sha256:4507071d81013fd03ea9930ec26bc8648b997927a11fa80e8ee81198b57e0ac7 \
     --hash=sha256:b5cfe535d14dc68dfc1d1bb4ac1209ea28235b91156e2bba8e250d291c3fb4f8 \
     # via pipenv
 virtualenv==15.2.0 \
     --hash=sha256:1d7e241b431e7afce47e77f8843a276f652699d1fa4f93b9d8ce0076fd7b0b54 \
     --hash=sha256:e8e05d4714a1c51a2f5921e62f547fcb0f713ebbe959e0a7f585cc8bef71d11f
 voluptuous==0.11.5 \
--- a/third_party/python/taskcluster/PKG-INFO
+++ b/third_party/python/taskcluster/PKG-INFO
@@ -1,11 +1,11 @@
 Metadata-Version: 1.1
 Name: taskcluster
-Version: 4.0.1
+Version: 6.0.0
 Summary: Python client for Taskcluster
 Home-page: https://github.com/taskcluster/taskcluster-client.py
 Author: John Ford
 Author-email: jhford@mozilla.com
 License: UNKNOWN
 Description: UNKNOWN
 Platform: UNKNOWN
 Classifier: Programming Language :: Python :: 2.7
--- a/third_party/python/taskcluster/README.md
+++ b/third_party/python/taskcluster/README.md
@@ -65,17 +65,17 @@ These query-string arguments are only su
 
 ## Sync vs Async
 
 The objects under `taskcluster` (e.g., `taskcluster.Queue`) are
 python2-compatible and operate synchronously.
 
 
 The objects under `taskcluster.aio` (e.g., `taskcluster.aio.Queue`) require
-`python>=3.5`. The async objects use asyncio coroutines for concurrency; this
+`python>=3.6`. The async objects use asyncio coroutines for concurrency; this
 allows us to put I/O operations in the background, so operations that require
 the cpu can happen sooner. Given dozens of operations that can run concurrently
 (e.g., cancelling a medium-to-large task graph), this can result in significant
 performance improvements. The code would look something like
 
 ```python
 #!/usr/bin/env python
 import aiohttp
@@ -96,17 +96,20 @@ Other async code examples are available 
 Here's a slide deck for an [introduction to async python](https://gitpitch.com/escapewindow/slides-sf-2017/async-python).
 
 ## Usage
 
 * Here's a simple command:
 
     ```python
     import taskcluster
-    index = taskcluster.Index({'credentials': {'clientId': 'id', 'accessToken': 'accessToken'}})
+    index = taskcluster.Index({
+      'rootUrl': 'https://tc.example.com',
+      'credentials': {'clientId': 'id', 'accessToken': 'accessToken'},
+    })
     index.ping()
     ```
 
 * There are four calling conventions for methods:
 
     ```python
     client.method(v1, v1, payload)
     client.method(payload, k1=v1, k2=v2)
@@ -114,28 +117,44 @@ Here's a slide deck for an [introduction
     client.method(v1, v2, payload=payload, query=query)
     ```
 
 * Options for the topic exchange methods can be in the form of either a single
   dictionary argument or keyword arguments.  Only one form is allowed
 
     ```python
     from taskcluster import client
-    qEvt = client.QueueEvents()
+    qEvt = client.QueueEvents({rootUrl: 'https://tc.example.com'})
     # The following calls are equivalent
     qEvt.taskCompleted({'taskId': 'atask'})
     qEvt.taskCompleted(taskId='atask')
     ```
 
+## Root URL
+
+This client requires a `rootUrl` argument to identify the Taskcluster
+deployment to talk to.  As of this writing, the production cluster has rootUrl
+`https://taskcluster.net`.
+
+## Environment Variables
+
+As of version 6.0.0, the client does not read the standard `TASKCLUSTER_…`
+environment variables automatically.  To fetch their values explicitly, use
+`taskcluster.optionsFromEnvironment()`:
+
+```python
+auth = taskcluster.Auth(taskcluster.optionsFromEnvironment())
+```
+
 ## Pagination
 There are two ways to accomplish pagination easily with the python client.  The first is
 to implement pagination in your code:
 ```python
 import taskcluster
-queue = taskcluster.Queue()
+queue = taskcluster.Queue({'rootUrl': 'https://tc.example.com'})
 i = 0
 tasks = 0
 outcome = queue.listTaskGroup('JzTGxwxhQ76_Tt1dxkaG5g')
 while outcome.get('continuationToken'):
     print('Response %d gave us %d more tasks' % (i, len(outcome['tasks'])))
     if outcome.get('continuationToken'):
         outcome = queue.listTaskGroup('JzTGxwxhQ76_Tt1dxkaG5g', query={'continuationToken': outcome.get('continuationToken')})
     i += 1
@@ -148,17 +167,17 @@ in the sync client.  This feature allows
 'paginationHandler' keyword-argument.  This function will be passed the
 response body of the API method as its sole positional arugment.
 
 This example of the built in pagination shows how a list of tasks could be
 built and then counted:
 
 ```python
 import taskcluster
-queue = taskcluster.Queue()
+queue = taskcluster.Queue({'rootUrl': 'https://tc.example.com'})
 
 responses = []
 
 def handle_page(y):
     print("%d tasks fetched" % len(y.get('tasks', [])))
     responses.append(y)
 
 queue.listTaskGroup('JzTGxwxhQ76_Tt1dxkaG5g', paginationHandler=handle_page)
@@ -1006,19 +1025,19 @@ await asyncAuth.testAuthenticateGet() # 
 
 
 ### Exchanges in `taskcluster.AuthEvents`
 ```python
 // Create AuthEvents client instance
 import taskcluster
 authEvents = taskcluster.AuthEvents(options)
 ```
-The auth service, typically available at `auth.taskcluster.net`
-is responsible for storing credentials, managing assignment of scopes,
-and validation of request signatures from other services.
+The auth service is responsible for storing credentials, managing
+assignment of scopes, and validation of request signatures from other
+services.
 
 These exchanges provides notifications when credentials or roles are
 updated. This is mostly so that multiple instances of the auth service
 can purge their caches and synchronize state. But you are of course
 welcome to use these for other purposes, monitoring changes for example.
 #### Client Created Messages
  * `authEvents.clientCreated(routingKeyPattern) -> routingKey`
    * `reserved` Description: Space reserved for future routing-key entries, you should always match this entry with `#`. As automatically done by our tooling, if not specified.
@@ -1483,21 +1502,33 @@ import taskcluster.aio
 
 eC2Manager = taskcluster.EC2Manager(options)
 # Below only for async instances, assume already in coroutine
 loop = asyncio.get_event_loop()
 session = taskcluster.aio.createSession(loop=loop)
 asyncEC2Manager = taskcluster.aio.EC2Manager(options, session=session)
 ```
 A taskcluster service which manages EC2 instances.  This service does not understand any taskcluster concepts intrinsicaly other than using the name `workerType` to refer to a group of associated instances.  Unless you are working on building a provisioner for AWS, you almost certainly do not want to use this service
+#### Ping Server
+Respond without doing anything.
+This endpoint is used to check that the service is up.
+
+
+```python
+# Sync calls
+eC2Manager.ping() # -> None`
+# Async call
+await asyncEC2Manager.ping() # -> None
+```
+
 #### See the list of worker types which are known to be managed
 This method is only for debugging the ec2-manager
 
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/list-worker-types.json#)
+Required [output schema](v1/list-worker-types.json#)
 
 ```python
 # Sync calls
 eC2Manager.listWorkerTypes() # -> result`
 # Async call
 await asyncEC2Manager.listWorkerTypes() # -> result
 ```
 
@@ -1505,17 +1536,17 @@ await asyncEC2Manager.listWorkerTypes() 
 Request an instance of a worker type
 
 
 
 Takes the following arguments:
 
   * `workerType`
 
-Required [input schema](http://schemas.taskcluster.net/ec2-manager/v1/run-instance-request.json#)
+Required [input schema](v1/run-instance-request.json#)
 
 ```python
 # Sync calls
 eC2Manager.runInstance(workerType, payload) # -> None`
 eC2Manager.runInstance(payload, workerType='value') # -> None
 # Async call
 await asyncEC2Manager.runInstance(workerType, payload) # -> None
 await asyncEC2Manager.runInstance(payload, workerType='value') # -> None
@@ -1543,17 +1574,17 @@ await asyncEC2Manager.terminateWorkerTyp
 Return an object which has a generic state description. This only contains counts of instances
 
 
 
 Takes the following arguments:
 
   * `workerType`
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/worker-type-resources.json#)
+Required [output schema](v1/worker-type-resources.json#)
 
 ```python
 # Sync calls
 eC2Manager.workerTypeStats(workerType) # -> result`
 eC2Manager.workerTypeStats(workerType='value') # -> result
 # Async call
 await asyncEC2Manager.workerTypeStats(workerType) # -> result
 await asyncEC2Manager.workerTypeStats(workerType='value') # -> result
@@ -1563,17 +1594,17 @@ await asyncEC2Manager.workerTypeStats(wo
 Return a view of the health of a given worker type
 
 
 
 Takes the following arguments:
 
   * `workerType`
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/health.json#)
+Required [output schema](v1/health.json#)
 
 ```python
 # Sync calls
 eC2Manager.workerTypeHealth(workerType) # -> result`
 eC2Manager.workerTypeHealth(workerType='value') # -> result
 # Async call
 await asyncEC2Manager.workerTypeHealth(workerType) # -> result
 await asyncEC2Manager.workerTypeHealth(workerType='value') # -> result
@@ -1583,17 +1614,17 @@ await asyncEC2Manager.workerTypeHealth(w
 Return a list of the most recent errors encountered by a worker type
 
 
 
 Takes the following arguments:
 
   * `workerType`
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/errors.json#)
+Required [output schema](v1/errors.json#)
 
 ```python
 # Sync calls
 eC2Manager.workerTypeErrors(workerType) # -> result`
 eC2Manager.workerTypeErrors(workerType='value') # -> result
 # Async call
 await asyncEC2Manager.workerTypeErrors(workerType) # -> result
 await asyncEC2Manager.workerTypeErrors(workerType='value') # -> result
@@ -1603,17 +1634,17 @@ await asyncEC2Manager.workerTypeErrors(w
 Return state information for a given worker type
 
 
 
 Takes the following arguments:
 
   * `workerType`
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/worker-type-state.json#)
+Required [output schema](v1/worker-type-state.json#)
 
 ```python
 # Sync calls
 eC2Manager.workerTypeState(workerType) # -> result`
 eC2Manager.workerTypeState(workerType='value') # -> result
 # Async call
 await asyncEC2Manager.workerTypeState(workerType) # -> result
 await asyncEC2Manager.workerTypeState(workerType='value') # -> result
@@ -1623,17 +1654,17 @@ await asyncEC2Manager.workerTypeState(wo
 Idempotently ensure that a keypair of a given name exists
 
 
 
 Takes the following arguments:
 
   * `name`
 
-Required [input schema](http://schemas.taskcluster.net/ec2-manager/v1/create-key-pair.json#)
+Required [input schema](v1/create-key-pair.json#)
 
 ```python
 # Sync calls
 eC2Manager.ensureKeyPair(name, payload) # -> None`
 eC2Manager.ensureKeyPair(payload, name='value') # -> None
 # Async call
 await asyncEC2Manager.ensureKeyPair(name, payload) # -> None
 await asyncEC2Manager.ensureKeyPair(payload, name='value') # -> None
@@ -1675,58 +1706,58 @@ eC2Manager.terminateInstance(region='val
 await asyncEC2Manager.terminateInstance(region, instanceId) # -> None
 await asyncEC2Manager.terminateInstance(region='value', instanceId='value') # -> None
 ```
 
 #### Request prices for EC2
 Return a list of possible prices for EC2
 
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/prices.json#)
+Required [output schema](v1/prices.json#)
 
 ```python
 # Sync calls
 eC2Manager.getPrices() # -> result`
 # Async call
 await asyncEC2Manager.getPrices() # -> result
 ```
 
 #### Request prices for EC2
 Return a list of possible prices for EC2
 
 
-Required [input schema](http://schemas.taskcluster.net/ec2-manager/v1/prices-request.json#)
-
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/prices.json#)
+Required [input schema](v1/prices-request.json#)
+
+Required [output schema](v1/prices.json#)
 
 ```python
 # Sync calls
 eC2Manager.getSpecificPrices(payload) # -> result`
 # Async call
 await asyncEC2Manager.getSpecificPrices(payload) # -> result
 ```
 
 #### Get EC2 account health metrics
 Give some basic stats on the health of our EC2 account
 
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/health.json#)
+Required [output schema](v1/health.json#)
 
 ```python
 # Sync calls
 eC2Manager.getHealth() # -> result`
 # Async call
 await asyncEC2Manager.getHealth() # -> result
 ```
 
 #### Look up the most recent errors in the provisioner across all worker types
 Return a list of recent errors encountered
 
 
-Required [output schema](http://schemas.taskcluster.net/ec2-manager/v1/errors.json#)
+Required [output schema](v1/errors.json#)
 
 ```python
 # Sync calls
 eC2Manager.getRecentErrors() # -> result`
 # Async call
 await asyncEC2Manager.getRecentErrors() # -> result
 ```
 
@@ -1816,58 +1847,34 @@ This method is only for debugging the ec
 
 ```python
 # Sync calls
 eC2Manager.purgeQueues() # -> None`
 # Async call
 await asyncEC2Manager.purgeQueues() # -> None
 ```
 
-#### API Reference
-Generate an API reference for this service
-
-
-```python
-# Sync calls
-eC2Manager.apiReference() # -> None`
-# Async call
-await asyncEC2Manager.apiReference() # -> None
-```
-
-#### Ping Server
-Respond without doing anything.
-This endpoint is used to check that the service is up.
-
-
-```python
-# Sync calls
-eC2Manager.ping() # -> None`
-# Async call
-await asyncEC2Manager.ping() # -> None
-```
-
 
 
 
 ### Methods in `taskcluster.Github`
 ```python
 import asyncio # Only for async 
 // Create Github client instance
 import taskcluster
 import taskcluster.aio
 
 github = taskcluster.Github(options)
 # Below only for async instances, assume already in coroutine
 loop = asyncio.get_event_loop()
 session = taskcluster.aio.createSession(loop=loop)
 asyncGithub = taskcluster.aio.Github(options, session=session)
 ```
-The github service, typically available at
-`github.taskcluster.net`, is responsible for publishing pulse
-messages in response to GitHub events.
+The github service is responsible for creating tasks in reposnse
+to GitHub events, and posting results to the GitHub UI.
 
 This document describes the API end-point for consuming GitHub
 web hooks, as well as some useful consumer APIs.
 
 When Github forbids an action, this service returns an HTTP 403
 with code ForbiddenByGithub.
 #### Ping Server
 Respond without doing anything.
@@ -2051,16 +2058,22 @@ github service
    * `repository` is required  Description: The GitHub `repository` which had an event.All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped.
 
 #### GitHub release Event
  * `githubEvents.release(routingKeyPattern) -> routingKey`
    * `routingKeyKind` is constant of `primary`  is required  Description: Identifier for the routing-key kind. This is always `"primary"` for the formalized routing key.
    * `organization` is required  Description: The GitHub `organization` which had an event. All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped.
    * `repository` is required  Description: The GitHub `repository` which had an event.All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped.
 
+#### GitHub release Event
+ * `githubEvents.taskGroupDefined(routingKeyPattern) -> routingKey`
+   * `routingKeyKind` is constant of `primary`  is required  Description: Identifier for the routing-key kind. This is always `"primary"` for the formalized routing key.
+   * `organization` is required  Description: The GitHub `organization` which had an event. All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped.
+   * `repository` is required  Description: The GitHub `repository` which had an event.All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped.
+
 
 
 
 ### Methods in `taskcluster.Hooks`
 ```python
 import asyncio # Only for async 
 // Create Hooks client instance
 import taskcluster
@@ -2084,17 +2097,17 @@ scopes in `task.scopes`.  The new task h
 
 Hooks can have a "schedule" indicating specific times that new tasks should
 be created.  Each schedule is in a simple cron format, per 
 https://www.npmjs.com/package/cron-parser.  For example:
  * `['0 0 1 * * *']` -- daily at 1:00 UTC
  * `['0 0 9,21 * * 1-5', '0 0 12 * * 0,6']` -- weekdays at 9:00 and 21:00 UTC, weekends at noon
 
 The task definition is used as a JSON-e template, with a context depending on how it is fired.  See
-https://docs.taskcluster.net/reference/core/taskcluster-hooks/docs/firing-hooks
+[/docs/reference/core/taskcluster-hooks/docs/firing-hooks](firing-hooks)
 for more information.
 #### Ping Server
 Respond without doing anything.
 This endpoint is used to check that the service is up.
 
 
 ```python
 # Sync calls
@@ -2618,68 +2631,68 @@ import taskcluster.aio
 login = taskcluster.Login(options)
 # Below only for async instances, assume already in coroutine
 loop = asyncio.get_event_loop()
 session = taskcluster.aio.createSession(loop=loop)
 asyncLogin = taskcluster.aio.Login(options, session=session)
 ```
 The Login service serves as the interface between external authentication
 systems and Taskcluster credentials.
+#### Ping Server
+Respond without doing anything.
+This endpoint is used to check that the service is up.
+
+
+```python
+# Sync calls
+login.ping() # -> None`
+# Async call
+await asyncLogin.ping() # -> None
+```
+
 #### Get Taskcluster credentials given a suitable `access_token`
 Given an OIDC `access_token` from a trusted OpenID provider, return a
 set of Taskcluster credentials for use on behalf of the identified
 user.
 
 This method is typically not called with a Taskcluster client library
 and does not accept Hawk credentials. The `access_token` should be
 given in an `Authorization` header:
 ```
 Authorization: Bearer abc.xyz
 ```
 
 The `access_token` is first verified against the named
-:provider, then passed to the provider's API to retrieve a user
+:provider, then passed to the provider's APIBuilder to retrieve a user
 profile. That profile is then used to generate Taskcluster credentials
 appropriate to the user. Note that the resulting credentials may or may
 not include a `certificate` property. Callers should be prepared for either
 alternative.
 
 The given credentials will expire in a relatively short time. Callers should
 monitor this expiration and refresh the credentials if necessary, by calling
 this endpoint again, if they have expired.
 
 
 
 Takes the following arguments:
 
   * `provider`
 
-Required [output schema](http://schemas.taskcluster.net/login/v1/oidc-credentials-response.json)
+Required [output schema](v1/oidc-credentials-response.json#)
 
 ```python
 # Sync calls
 login.oidcCredentials(provider) # -> result`
 login.oidcCredentials(provider='value') # -> result
 # Async call
 await asyncLogin.oidcCredentials(provider) # -> result
 await asyncLogin.oidcCredentials(provider='value') # -> result
 ```
 
-#### Ping Server
-Respond without doing anything.
-This endpoint is used to check that the service is up.
-
-
-```python
-# Sync calls
-login.ping() # -> None`
-# Async call
-await asyncLogin.ping() # -> None
-```
-
 
 
 
 ### Methods in `taskcluster.Notify`
 ```python
 import asyncio # Only for async 
 // Create Notify client instance
 import taskcluster
@@ -2756,31 +2769,135 @@ Required [input schema](v1/irc-request.j
 notify.irc(payload) # -> None`
 # Async call
 await asyncNotify.irc(payload) # -> None
 ```
 
 
 
 
+### Methods in `taskcluster.Pulse`
+```python
+import asyncio # Only for async 
+// Create Pulse client instance
+import taskcluster
+import taskcluster.aio
+
+pulse = taskcluster.Pulse(options)
+# Below only for async instances, assume already in coroutine
+loop = asyncio.get_event_loop()
+session = taskcluster.aio.createSession(loop=loop)
+asyncPulse = taskcluster.aio.Pulse(options, session=session)
+```
+The taskcluster-pulse service, typically available at `pulse.taskcluster.net`
+manages pulse credentials for taskcluster users.
+
+A service to manage Pulse credentials for anything using
+Taskcluster credentials. This allows for self-service pulse
+access and greater control within the Taskcluster project.
+#### Ping Server
+Respond without doing anything.
+This endpoint is used to check that the service is up.
+
+
+```python
+# Sync calls
+pulse.ping() # -> None`
+# Async call
+await asyncPulse.ping() # -> None
+```
+
+#### List Namespaces
+List the namespaces managed by this service.
+
+This will list up to 1000 namespaces. If more namespaces are present a
+`continuationToken` will be returned, which can be given in the next
+request. For the initial request, do not provide continuation token.
+
+
+Required [output schema](v1/list-namespaces-response.json#)
+
+```python
+# Sync calls
+pulse.listNamespaces() # -> result`
+# Async call
+await asyncPulse.listNamespaces() # -> result
+```
+
+#### Get a namespace
+Get public information about a single namespace. This is the same information
+as returned by `listNamespaces`.
+
+
+
+Takes the following arguments:
+
+  * `namespace`
+
+Required [output schema](v1/namespace.json#)
+
+```python
+# Sync calls
+pulse.namespace(namespace) # -> result`
+pulse.namespace(namespace='value') # -> result
+# Async call
+await asyncPulse.namespace(namespace) # -> result
+await asyncPulse.namespace(namespace='value') # -> result
+```
+
+#### Claim a namespace
+Claim a namespace, returning a connection string with access to that namespace
+good for use until the `reclaimAt` time in the response body. The connection
+string can be used as many times as desired during this period, but must not
+be used after `reclaimAt`.
+
+Connections made with this connection string may persist beyond `reclaimAt`,
+although it should not persist forever.  24 hours is a good maximum, and this
+service will terminate connections after 72 hours (although this value is
+configurable).
+
+The specified `expires` time updates any existing expiration times.  Connections
+for expired namespaces will be terminated.
+
+
+
+Takes the following arguments:
+
+  * `namespace`
+
+Required [input schema](v1/namespace-request.json#)
+
+Required [output schema](v1/namespace-response.json#)
+
+```python
+# Sync calls
+pulse.claimNamespace(namespace, payload) # -> result`
+pulse.claimNamespace(payload, namespace='value') # -> result
+# Async call
+await asyncPulse.claimNamespace(namespace, payload) # -> result
+await asyncPulse.claimNamespace(payload, namespace='value') # -> result
+```
+
+
+
+
 ### Methods in `taskcluster.PurgeCache`
 ```python
 import asyncio # Only for async 
 // Create PurgeCache client instance
 import taskcluster
 import taskcluster.aio
 
 purgeCache = taskcluster.PurgeCache(options)
 # Below only for async instances, assume already in coroutine
 loop = asyncio.get_event_loop()
 session = taskcluster.aio.createSession(loop=loop)
 asyncPurgeCache = taskcluster.aio.PurgeCache(options, session=session)
 ```
-The purge-cache service, typically available at
-`purge-cache.taskcluster.net`, is responsible for publishing a pulse
+The purge-cache service is responsible for publishing a pulse
 message for workers, so they can purge cache upon request.
 
 This document describes the API end-point for publishing the pulse
 message. This is mainly intended to be used by tools.
 #### Ping Server
 Respond without doing anything.
 This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/setup.py
+++ b/third_party/python/taskcluster/setup.py
@@ -2,17 +2,17 @@
 
 from setuptools import setup
 from setuptools.command.test import test as TestCommand
 import sys
 
 # The VERSION variable is automagically changed
 # by release.sh.  Make sure you understand how
 # that script works if you want to change this
-VERSION = '4.0.1'
+VERSION = '6.0.0'
 
 tests_require = [
     'nose==1.3.7',
     'nose-exclude==0.5.0',
     'httmock==1.2.6',
     'rednose==1.2.1',
     'mock==1.0.1',
     'setuptools-lint==0.3',
@@ -25,16 +25,17 @@ tests_require = [
 ]
 
 # requests has a policy of not breaking apis between major versions
 # http://docs.python-requests.org/en/latest/community/release-process/
 install_requires = [
     'requests>=2.4.3,<3',
     'mohawk>=0.3.4,<0.4',
     'slugid>=1.0.7,<2',
+    'taskcluster-urls>=10.1.0,<12',
     'six>=1.10.0,<2',
 ]
 
 # from http://testrun.org/tox/latest/example/basic.html
 class Tox(TestCommand):
     user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
 
     def initialize_options(self):
--- a/third_party/python/taskcluster/taskcluster/_client_importer.py
+++ b/third_party/python/taskcluster/taskcluster/_client_importer.py
@@ -4,14 +4,15 @@ from .awsprovisioner import AwsProvision
 from .awsprovisionerevents import AwsProvisionerEvents  # NOQA
 from .ec2manager import EC2Manager  # NOQA
 from .github import Github  # NOQA
 from .githubevents import GithubEvents  # NOQA
 from .hooks import Hooks  # NOQA
 from .index import Index  # NOQA
 from .login import Login  # NOQA
 from .notify import Notify  # NOQA
+from .pulse import Pulse  # NOQA
 from .purgecache import PurgeCache  # NOQA
 from .purgecacheevents import PurgeCacheEvents  # NOQA
 from .queue import Queue  # NOQA
 from .queueevents import QueueEvents  # NOQA
 from .secrets import Secrets  # NOQA
 from .treeherderevents import TreeherderEvents  # NOQA
--- a/third_party/python/taskcluster/taskcluster/aio/_client_importer.py
+++ b/third_party/python/taskcluster/taskcluster/aio/_client_importer.py
@@ -4,14 +4,15 @@ from .awsprovisioner import AwsProvision
 from .awsprovisionerevents import AwsProvisionerEvents  # NOQA
 from .ec2manager import EC2Manager  # NOQA
 from .github import Github  # NOQA
 from .githubevents import GithubEvents  # NOQA
 from .hooks import Hooks  # NOQA
 from .index import Index  # NOQA
 from .login import Login  # NOQA
 from .notify import Notify  # NOQA
+from .pulse import Pulse  # NOQA
 from .purgecache import PurgeCache  # NOQA
 from .purgecacheevents import PurgeCacheEvents  # NOQA
 from .queue import Queue  # NOQA
 from .queueevents import QueueEvents  # NOQA
 from .secrets import Secrets  # NOQA
 from .treeherderevents import TreeherderEvents  # NOQA
--- a/third_party/python/taskcluster/taskcluster/aio/asyncclient.py
+++ b/third_party/python/taskcluster/taskcluster/aio/asyncclient.py
@@ -109,17 +109,17 @@ class AsyncBaseClient(BaseClient):
         else:
             return response
 
     async def _makeHttpRequest(self, method, route, payload):
         """ Make an HTTP Request for the API endpoint.  This method wraps
         the logic about doing failure retry and passes off the actual work
         of doing an HTTP request to another method."""
 
-        url = self._joinBaseUrlAndRoute(route)
+        url = self._constructUrl(route)
         log.debug('Full URL used is: %s', url)
 
         hawkExt = self.makeHawkExt()
 
         # Serialize payload if given
         if payload is not None:
             payload = utils.dumpJson(payload)
 
@@ -216,17 +216,17 @@ class AsyncBaseClient(BaseClient):
                     body=data,
                     superExc=None
                 )
 
             # Try to load JSON
             try:
                 await response.release()
                 return await response.json()
-            except ValueError:
+            except (ValueError, aiohttp.client_exceptions.ContentTypeError):
                 return {"response": response}
 
         # This code-path should be unreachable
         assert False, "Error from last retry should have been raised!"
 
     async def __aenter__(self):
         if self._implicitSession and not self.session:
             self.session = createSession()
@@ -234,29 +234,41 @@ class AsyncBaseClient(BaseClient):
 
     async def __aexit__(self, *args):
         if self._implicitSession and self.session:
             await self.session.close()
             self.session = None
 
 
 def createApiClient(name, api):
+    api = api['reference']
+
     attributes = dict(
         name=name,
         __doc__=api.get('description'),
         classOptions={},
         funcinfo={},
     )
 
-    copiedOptions = ('baseUrl', 'exchangePrefix')
+    # apply a default for apiVersion; this can be removed when all services
+    # have apiVersion
+    if 'apiVersion' not in api:
+        api['apiVersion'] = 'v1'
+
+    copiedOptions = ('exchangePrefix',)
     for opt in copiedOptions:
-        if opt in api['reference']:
-            attributes['classOptions'][opt] = api['reference'][opt]
+        if opt in api:
+            attributes['classOptions'][opt] = api[opt]
 
-    for entry in api['reference']['entries']:
+    copiedProperties = ('serviceName', 'apiVersion')
+    for opt in copiedProperties:
+        if opt in api:
+            attributes[opt] = api[opt]
+
+    for entry in api['entries']:
         if entry['type'] == 'function':
             def addApiCall(e):
                 async def apiCall(self, *args, **kwargs):
                     return await self._makeApiCall(e, *args, **kwargs)
                 return apiCall
             f = addApiCall(entry)
 
             docStr = "Call the %s api's %s method.  " % (name, entry['name'])
--- a/third_party/python/taskcluster/taskcluster/aio/asyncutils.py
+++ b/third_party/python/taskcluster/taskcluster/aio/asyncutils.py
@@ -106,11 +106,11 @@ async def makeSingleHttpRequest(method, 
         if implicit:
             await session.close()
 
 
 async def putFile(filename, url, contentType, session=None):
     with open(filename, 'rb') as f:
         contentLength = os.fstat(f.fileno()).st_size
         return await makeHttpRequest('put', url, f, headers={
-            'Content-Length': contentLength,
+            'Content-Length': str(contentLength),
             'Content-Type': contentType,
         }, session=session)
--- a/third_party/python/taskcluster/taskcluster/aio/auth.py
+++ b/third_party/python/taskcluster/taskcluster/aio/auth.py
@@ -50,18 +50,19 @@ class Auth(AsyncBaseClient):
     The authentication service also has API end-points for delegating access
     to some guarded service such as AWS S3, or Azure Table Storage.
     Generally, we add API end-points to this server when we wish to use
     Taskcluster credentials to grant access to a third-party service used
     by many Taskcluster components.
     """
 
     classOptions = {
-        "baseUrl": "https://auth.taskcluster.net/v1/"
     }
+    serviceName = 'auth'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/authevents.py
+++ b/third_party/python/taskcluster/taskcluster/aio/authevents.py
@@ -8,29 +8,31 @@ from .asyncclient import createApiClient
 from .asyncclient import config
 from .asyncclient import createTemporaryCredentials
 from .asyncclient import createSession
 _defaultConfig = config
 
 
 class AuthEvents(AsyncBaseClient):
     """
-    The auth service, typically available at `auth.taskcluster.net`
-    is responsible for storing credentials, managing assignment of scopes,
-    and validation of request signatures from other services.
+    The auth service is responsible for storing credentials, managing
+    assignment of scopes, and validation of request signatures from other
+    services.
 
     These exchanges provides notifications when credentials or roles are
     updated. This is mostly so that multiple instances of the auth service
     can purge their caches and synchronize state. But you are of course
     welcome to use these for other purposes, monitoring changes for example.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-auth/v1/"
+        "exchangePrefix": "exchange/taskcluster-auth/v1/",
     }
+    serviceName = 'auth'
+    apiVersion = 'v1'
 
     def clientCreated(self, *args, **kwargs):
         """
         Client Created Messages
 
         Message that a new client has been created.
 
         This exchange outputs: ``v1/client-message.json#``This exchange takes the following keys:
--- a/third_party/python/taskcluster/taskcluster/aio/awsprovisioner.py
+++ b/third_party/python/taskcluster/taskcluster/aio/awsprovisioner.py
@@ -39,18 +39,19 @@ class AwsProvisioner(AsyncBaseClient):
     the worker's credentials and any needed passwords or other restricted
     information.  The worker is responsible for deleting the secret after
     retrieving it, to prevent dissemination of the secret to other proceses
     which can read the instance user data.
 
     """
 
     classOptions = {
-        "baseUrl": "https://aws-provisioner.taskcluster.net/v1"
     }
+    serviceName = 'aws-provisioner'
+    apiVersion = 'v1'
 
     async def listWorkerTypeSummaries(self, *args, **kwargs):
         """
         List worker types with details
 
         Return a list of worker types, including some summary information about
         current capacity for each.  While this list includes all defined worker types,
         there may be running EC2 instances for deleted worker types that are not
--- a/third_party/python/taskcluster/taskcluster/aio/awsprovisionerevents.py
+++ b/third_party/python/taskcluster/taskcluster/aio/awsprovisionerevents.py
@@ -12,18 +12,19 @@ from .asyncclient import createSession
 
 
 class AwsProvisionerEvents(AsyncBaseClient):
     """
     Exchanges from the provisioner... more docs later
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-aws-provisioner/v1/"
+        "exchangePrefix": "exchange/taskcluster-aws-provisioner/v1/",
     }
+    apiVersion = 'v1'
 
     def workerTypeCreated(self, *args, **kwargs):
         """
         WorkerType Created Message
 
         When a new `workerType` is created a message will be published to this
         exchange.
 
--- a/third_party/python/taskcluster/taskcluster/aio/ec2manager.py
+++ b/third_party/python/taskcluster/taskcluster/aio/ec2manager.py
@@ -12,39 +12,52 @@ from .asyncclient import createSession
 
 
 class EC2Manager(AsyncBaseClient):
     """
     A taskcluster service which manages EC2 instances.  This service does not understand any taskcluster concepts intrinsicaly other than using the name `workerType` to refer to a group of associated instances.  Unless you are working on building a provisioner for AWS, you almost certainly do not want to use this service
     """
 
     classOptions = {
-        "baseUrl": "https://ec2-manager.taskcluster.net/v1"
     }
+    serviceName = 'ec2-manager'
+    apiVersion = 'v1'
+
+    async def ping(self, *args, **kwargs):
+        """
+        Ping Server
+
+        Respond without doing anything.
+        This endpoint is used to check that the service is up.
+
+        This method is ``stable``
+        """
+
+        return await self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
 
     async def listWorkerTypes(self, *args, **kwargs):
         """
         See the list of worker types which are known to be managed
 
         This method is only for debugging the ec2-manager
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/list-worker-types.json#``
+        This method gives output: ``v1/list-worker-types.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["listWorkerTypes"], *args, **kwargs)
 
     async def runInstance(self, *args, **kwargs):
         """
         Run an instance
 
         Request an instance of a worker type
 
-        This method takes input: ``http://schemas.taskcluster.net/ec2-manager/v1/run-instance-request.json#``
+        This method takes input: ``v1/run-instance-request.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["runInstance"], *args, **kwargs)
 
     async def terminateWorkerType(self, *args, **kwargs):
         """
@@ -58,69 +71,69 @@ class EC2Manager(AsyncBaseClient):
         return await self._makeApiCall(self.funcinfo["terminateWorkerType"], *args, **kwargs)
 
     async def workerTypeStats(self, *args, **kwargs):
         """
         Look up the resource stats for a workerType
 
         Return an object which has a generic state description. This only contains counts of instances
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/worker-type-resources.json#``
+        This method gives output: ``v1/worker-type-resources.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["workerTypeStats"], *args, **kwargs)
 
     async def workerTypeHealth(self, *args, **kwargs):
         """
         Look up the resource health for a workerType
 
         Return a view of the health of a given worker type
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/health.json#``
+        This method gives output: ``v1/health.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["workerTypeHealth"], *args, **kwargs)
 
     async def workerTypeErrors(self, *args, **kwargs):
         """
         Look up the most recent errors of a workerType
 
         Return a list of the most recent errors encountered by a worker type
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/errors.json#``
+        This method gives output: ``v1/errors.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["workerTypeErrors"], *args, **kwargs)
 
     async def workerTypeState(self, *args, **kwargs):
         """
         Look up the resource state for a workerType
 
         Return state information for a given worker type
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/worker-type-state.json#``
+        This method gives output: ``v1/worker-type-state.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["workerTypeState"], *args, **kwargs)
 
     async def ensureKeyPair(self, *args, **kwargs):
         """
         Ensure a KeyPair for a given worker type exists
 
         Idempotently ensure that a keypair of a given name exists
 
-        This method takes input: ``http://schemas.taskcluster.net/ec2-manager/v1/create-key-pair.json#``
+        This method takes input: ``v1/create-key-pair.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["ensureKeyPair"], *args, **kwargs)
 
     async def removeKeyPair(self, *args, **kwargs):
         """
@@ -145,58 +158,58 @@ class EC2Manager(AsyncBaseClient):
         return await self._makeApiCall(self.funcinfo["terminateInstance"], *args, **kwargs)
 
     async def getPrices(self, *args, **kwargs):
         """
         Request prices for EC2
 
         Return a list of possible prices for EC2
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/prices.json#``
+        This method gives output: ``v1/prices.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["getPrices"], *args, **kwargs)
 
     async def getSpecificPrices(self, *args, **kwargs):
         """
         Request prices for EC2
 
         Return a list of possible prices for EC2
 
-        This method takes input: ``http://schemas.taskcluster.net/ec2-manager/v1/prices-request.json#``
+        This method takes input: ``v1/prices-request.json#``
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/prices.json#``
+        This method gives output: ``v1/prices.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["getSpecificPrices"], *args, **kwargs)
 
     async def getHealth(self, *args, **kwargs):
         """
         Get EC2 account health metrics
 
         Give some basic stats on the health of our EC2 account
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/health.json#``
+        This method gives output: ``v1/health.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["getHealth"], *args, **kwargs)
 
     async def getRecentErrors(self, *args, **kwargs):
         """
         Look up the most recent errors in the provisioner across all worker types
 
         Return a list of recent errors encountered
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/errors.json#``
+        This method gives output: ``v1/errors.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["getRecentErrors"], *args, **kwargs)
 
     async def regions(self, *args, **kwargs):
         """
@@ -284,121 +297,91 @@ class EC2Manager(AsyncBaseClient):
 
         This method is only for debugging the ec2-manager
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["purgeQueues"], *args, **kwargs)
 
-    async def apiReference(self, *args, **kwargs):
-        """
-        API Reference
-
-        Generate an API reference for this service
-
-        This method is ``experimental``
-        """
-
-        return await self._makeApiCall(self.funcinfo["apiReference"], *args, **kwargs)
-
-    async def ping(self, *args, **kwargs):
-        """
-        Ping Server
-
-        Respond without doing anything.
-        This endpoint is used to check that the service is up.
-
-        This method is ``stable``
-        """
-
-        return await self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
-
     funcinfo = {
         "allState": {
             'args': [],
             'method': 'get',
             'name': 'allState',
             'route': '/internal/all-state',
             'stability': 'experimental',
         },
         "amiUsage": {
             'args': [],
             'method': 'get',
             'name': 'amiUsage',
             'route': '/internal/ami-usage',
             'stability': 'experimental',
         },
-        "apiReference": {
-            'args': [],
-            'method': 'get',
-            'name': 'apiReference',
-            'route': '/internal/api-reference',
-            'stability': 'experimental',
-        },
         "dbpoolStats": {
             'args': [],
             'method': 'get',
             'name': 'dbpoolStats',
             'route': '/internal/db-pool-stats',
             'stability': 'experimental',
         },
         "ebsUsage": {
             'args': [],
             'method': 'get',
             'name': 'ebsUsage',
             'route': '/internal/ebs-usage',
             'stability': 'experimental',
         },
         "ensureKeyPair": {
             'args': ['name'],
-            'input': 'http://schemas.taskcluster.net/ec2-manager/v1/create-key-pair.json#',
+            'input': 'v1/create-key-pair.json#',
             'method': 'get',
             'name': 'ensureKeyPair',
             'route': '/key-pairs/<name>',
             'stability': 'experimental',
         },
         "getHealth": {
             'args': [],
             'method': 'get',
             'name': 'getHealth',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/health.json#',
+            'output': 'v1/health.json#',
             'route': '/health',
             'stability': 'experimental',
         },
         "getPrices": {
             'args': [],
             'method': 'get',
             'name': 'getPrices',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/prices.json#',
+            'output': 'v1/prices.json#',
             'route': '/prices',
             'stability': 'experimental',
         },
         "getRecentErrors": {
             'args': [],
             'method': 'get',
             'name': 'getRecentErrors',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/errors.json#',
+            'output': 'v1/errors.json#',
             'route': '/errors',
             'stability': 'experimental',
         },
         "getSpecificPrices": {
             'args': [],
-            'input': 'http://schemas.taskcluster.net/ec2-manager/v1/prices-request.json#',
+            'input': 'v1/prices-request.json#',
             'method': 'post',
             'name': 'getSpecificPrices',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/prices.json#',
+            'output': 'v1/prices.json#',
             'route': '/prices',
             'stability': 'experimental',
         },
         "listWorkerTypes": {
             'args': [],
             'method': 'get',
             'name': 'listWorkerTypes',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/list-worker-types.json#',
+            'output': 'v1/list-worker-types.json#',
             'route': '/worker-types',
             'stability': 'experimental',
         },
         "ping": {
             'args': [],
             'method': 'get',
             'name': 'ping',
             'route': '/ping',
@@ -422,17 +405,17 @@ class EC2Manager(AsyncBaseClient):
             'args': ['name'],
             'method': 'delete',
             'name': 'removeKeyPair',
             'route': '/key-pairs/<name>',
             'stability': 'experimental',
         },
         "runInstance": {
             'args': ['workerType'],
-            'input': 'http://schemas.taskcluster.net/ec2-manager/v1/run-instance-request.json#',
+            'input': 'v1/run-instance-request.json#',
             'method': 'put',
             'name': 'runInstance',
             'route': '/worker-types/<workerType>/instance',
             'stability': 'experimental',
         },
         "sqsStats": {
             'args': [],
             'method': 'get',
@@ -453,40 +436,40 @@ class EC2Manager(AsyncBaseClient):
             'name': 'terminateWorkerType',
             'route': '/worker-types/<workerType>/resources',
             'stability': 'experimental',
         },
         "workerTypeErrors": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeErrors',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/errors.json#',
+            'output': 'v1/errors.json#',
             'route': '/worker-types/<workerType>/errors',
             'stability': 'experimental',
         },
         "workerTypeHealth": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeHealth',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/health.json#',
+            'output': 'v1/health.json#',
             'route': '/worker-types/<workerType>/health',
             'stability': 'experimental',
         },
         "workerTypeState": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeState',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/worker-type-state.json#',
+            'output': 'v1/worker-type-state.json#',
             'route': '/worker-types/<workerType>/state',
             'stability': 'experimental',
         },
         "workerTypeStats": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeStats',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/worker-type-resources.json#',
+            'output': 'v1/worker-type-resources.json#',
             'route': '/worker-types/<workerType>/stats',
             'stability': 'experimental',
         },
     }
 
 
 __all__ = ['createTemporaryCredentials', 'config', '_defaultConfig', 'createApiClient', 'createSession', 'EC2Manager']
--- a/third_party/python/taskcluster/taskcluster/aio/github.py
+++ b/third_party/python/taskcluster/taskcluster/aio/github.py
@@ -8,30 +8,30 @@ from .asyncclient import createApiClient
 from .asyncclient import config
 from .asyncclient import createTemporaryCredentials
 from .asyncclient import createSession
 _defaultConfig = config
 
 
 class Github(AsyncBaseClient):
     """
-    The github service, typically available at
-    `github.taskcluster.net`, is responsible for publishing pulse
-    messages in response to GitHub events.
+    The github service is responsible for creating tasks in reposnse
+    to GitHub events, and posting results to the GitHub UI.
 
     This document describes the API end-point for consuming GitHub
     web hooks, as well as some useful consumer APIs.
 
     When Github forbids an action, this service returns an HTTP 403
     with code ForbiddenByGithub.
     """
 
     classOptions = {
-        "baseUrl": "https://github.taskcluster.net/v1/"
     }
+    serviceName = 'github'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/githubevents.py
+++ b/third_party/python/taskcluster/taskcluster/aio/githubevents.py
@@ -17,18 +17,20 @@ class GithubEvents(AsyncBaseClient):
     message for supported github events, translating Github webhook
     events into pulse messages.
 
     This document describes the exchange offered by the taskcluster
     github service
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-github/v1/"
+        "exchangePrefix": "exchange/taskcluster-github/v1/",
     }
+    serviceName = 'github'
+    apiVersion = 'v1'
 
     def pullRequest(self, *args, **kwargs):
         """
         GitHub Pull Request Event
 
         When a GitHub pull request event is posted it will be broadcast on this
         exchange with the designated `organization` and `repository`
         in the routing-key along with event specific metadata in the payload.
@@ -143,13 +145,50 @@ class GithubEvents(AsyncBaseClient):
                     'multipleWords': False,
                     'name': 'repository',
                 },
             ],
             'schema': 'v1/github-release-message.json#',
         }
         return self._makeTopicExchange(ref, *args, **kwargs)
 
+    def taskGroupDefined(self, *args, **kwargs):
+        """
+        GitHub release Event
+
+        used for creating status indicators in GitHub UI using Statuses API
+
+        This exchange outputs: ``v1/task-group-defined-message.json#``This exchange takes the following keys:
+
+         * routingKeyKind: Identifier for the routing-key kind. This is always `"primary"` for the formalized routing key. (required)
+
+         * organization: The GitHub `organization` which had an event. All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped. (required)
+
+         * repository: The GitHub `repository` which had an event.All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped. (required)
+        """
+
+        ref = {
+            'exchange': 'task-group-defined',
+            'name': 'taskGroupDefined',
+            'routingKey': [
+                {
+                    'constant': 'primary',
+                    'multipleWords': False,
+                    'name': 'routingKeyKind',
+                },
+                {
+                    'multipleWords': False,
+                    'name': 'organization',
+                },
+                {
+                    'multipleWords': False,
+                    'name': 'repository',
+                },
+            ],
+            'schema': 'v1/task-group-defined-message.json#',
+        }
+        return self._makeTopicExchange(ref, *args, **kwargs)
+
     funcinfo = {
     }
 
 
 __all__ = ['createTemporaryCredentials', 'config', '_defaultConfig', 'createApiClient', 'createSession', 'GithubEvents']
--- a/third_party/python/taskcluster/taskcluster/aio/hooks.py
+++ b/third_party/python/taskcluster/taskcluster/aio/hooks.py
@@ -25,23 +25,24 @@ class Hooks(AsyncBaseClient):
 
     Hooks can have a "schedule" indicating specific times that new tasks should
     be created.  Each schedule is in a simple cron format, per
     https://www.npmjs.com/package/cron-parser.  For example:
      * `['0 0 1 * * *']` -- daily at 1:00 UTC
      * `['0 0 9,21 * * 1-5', '0 0 12 * * 0,6']` -- weekdays at 9:00 and 21:00 UTC, weekends at noon
 
     The task definition is used as a JSON-e template, with a context depending on how it is fired.  See
-    https://docs.taskcluster.net/reference/core/taskcluster-hooks/docs/firing-hooks
+    [/docs/reference/core/taskcluster-hooks/docs/firing-hooks](firing-hooks)
     for more information.
     """
 
     classOptions = {
-        "baseUrl": "https://hooks.taskcluster.net/v1/"
     }
+    serviceName = 'hooks'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/index.py
+++ b/third_party/python/taskcluster/taskcluster/aio/index.py
@@ -103,18 +103,19 @@ class Index(AsyncBaseClient):
     to listen for messages about these tasks. For
     example one could bind to `route.index.some-app.*.release-build`,
     and pick up all messages about release builds. Hence, it is a
     good idea to document task index hierarchies, as these make up extension
     points in their own.
     """
 
     classOptions = {
-        "baseUrl": "https://index.taskcluster.net/v1/"
     }
+    serviceName = 'index'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/login.py
+++ b/third_party/python/taskcluster/taskcluster/aio/login.py
@@ -13,18 +13,31 @@ from .asyncclient import createSession
 
 class Login(AsyncBaseClient):
     """
     The Login service serves as the interface between external authentication
     systems and Taskcluster credentials.
     """
 
     classOptions = {
-        "baseUrl": "https://login.taskcluster.net/v1"
     }
+    serviceName = 'login'
+    apiVersion = 'v1'
+
+    async def ping(self, *args, **kwargs):
+        """
+        Ping Server
+
+        Respond without doing anything.
+        This endpoint is used to check that the service is up.
+
+        This method is ``stable``
+        """
+
+        return await self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
 
     async def oidcCredentials(self, *args, **kwargs):
         """
         Get Taskcluster credentials given a suitable `access_token`
 
         Given an OIDC `access_token` from a trusted OpenID provider, return a
         set of Taskcluster credentials for use on behalf of the identified
         user.
@@ -32,51 +45,39 @@ class Login(AsyncBaseClient):
         This method is typically not called with a Taskcluster client library
         and does not accept Hawk credentials. The `access_token` should be
         given in an `Authorization` header:
         ```
         Authorization: Bearer abc.xyz
         ```
 
         The `access_token` is first verified against the named
-        :provider, then passed to the provider's API to retrieve a user
+        :provider, then passed to the provider's APIBuilder to retrieve a user
         profile. That profile is then used to generate Taskcluster credentials
         appropriate to the user. Note that the resulting credentials may or may
         not include a `certificate` property. Callers should be prepared for either
         alternative.
 
         The given credentials will expire in a relatively short time. Callers should
         monitor this expiration and refresh the credentials if necessary, by calling
         this endpoint again, if they have expired.
 
-        This method gives output: ``http://schemas.taskcluster.net/login/v1/oidc-credentials-response.json``
+        This method gives output: ``v1/oidc-credentials-response.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["oidcCredentials"], *args, **kwargs)
 
-    async def ping(self, *args, **kwargs):
-        """
-        Ping Server
-
-        Respond without doing anything.
-        This endpoint is used to check that the service is up.
-
-        This method is ``stable``
-        """
-
-        return await self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
-
     funcinfo = {
         "oidcCredentials": {
             'args': ['provider'],
             'method': 'get',
             'name': 'oidcCredentials',
-            'output': 'http://schemas.taskcluster.net/login/v1/oidc-credentials-response.json',
+            'output': 'v1/oidc-credentials-response.json#',
             'route': '/oidc-credentials/<provider>',
             'stability': 'experimental',
         },
         "ping": {
             'args': [],
             'method': 'get',
             'name': 'ping',
             'route': '/ping',
--- a/third_party/python/taskcluster/taskcluster/aio/notify.py
+++ b/third_party/python/taskcluster/taskcluster/aio/notify.py
@@ -14,18 +14,19 @@ from .asyncclient import createSession
 class Notify(AsyncBaseClient):
     """
     The notification service, typically available at `notify.taskcluster.net`
     listens for tasks with associated notifications and handles requests to
     send emails and post pulse messages.
     """
 
     classOptions = {
-        "baseUrl": "https://notify.taskcluster.net/v1/"
     }
+    serviceName = 'notify'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/pulse.py
+++ b/third_party/python/taskcluster/taskcluster/aio/pulse.py
@@ -17,132 +17,116 @@ class Pulse(AsyncBaseClient):
     manages pulse credentials for taskcluster users.
 
     A service to manage Pulse credentials for anything using
     Taskcluster credentials. This allows for self-service pulse
     access and greater control within the Taskcluster project.
     """
 
     classOptions = {
-        "baseUrl": "https://pulse.taskcluster.net/v1"
     }
+    serviceName = 'pulse'
+    apiVersion = 'v1'
 
-    async def overview(self, *args, **kwargs):
+    async def ping(self, *args, **kwargs):
         """
-        Rabbit Overview
-
-        Get an overview of the Rabbit cluster.
+        Ping Server
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/rabbit-overview.json``
+        Respond without doing anything.
+        This endpoint is used to check that the service is up.
 
-        This method is ``experimental``
+        This method is ``stable``
         """
 
-        return await self._makeApiCall(self.funcinfo["overview"], *args, **kwargs)
+        return await self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
 
     async def listNamespaces(self, *args, **kwargs):
         """
         List Namespaces
 
         List the namespaces managed by this service.
 
         This will list up to 1000 namespaces. If more namespaces are present a
         `continuationToken` will be returned, which can be given in the next
-        request. For the initial request, do not provide continuation.
+        request. For the initial request, do not provide continuation token.
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/list-namespaces-response.json``
+        This method gives output: ``v1/list-namespaces-response.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["listNamespaces"], *args, **kwargs)
 
     async def namespace(self, *args, **kwargs):
         """
         Get a namespace
 
         Get public information about a single namespace. This is the same information
         as returned by `listNamespaces`.
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/namespace.json``
+        This method gives output: ``v1/namespace.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["namespace"], *args, **kwargs)
 
     async def claimNamespace(self, *args, **kwargs):
         """
         Claim a namespace
 
-        Claim a namespace, returning a username and password with access to that
-        namespace good for a short time.  Clients should call this endpoint again
-        at the re-claim time given in the response, as the password will be rotated
-        soon after that time.  The namespace will expire, and any associated queues
-        and exchanges will be deleted, at the given expiration time.
+        Claim a namespace, returning a connection string with access to that namespace
+        good for use until the `reclaimAt` time in the response body. The connection
+        string can be used as many times as desired during this period, but must not
+        be used after `reclaimAt`.
 
-        The `expires` and `contact` properties can be updated at any time in a reclaim
-        operation.
+        Connections made with this connection string may persist beyond `reclaimAt`,
+        although it should not persist forever.  24 hours is a good maximum, and this
+        service will terminate connections after 72 hours (although this value is
+        configurable).
 
-        This method takes input: ``http://schemas.taskcluster.net/pulse/v1/namespace-request.json``
+        The specified `expires` time updates any existing expiration times.  Connections
+        for expired namespaces will be terminated.
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/namespace-response.json``
+        This method takes input: ``v1/namespace-request.json#``
+
+        This method gives output: ``v1/namespace-response.json#``
 
         This method is ``experimental``
         """
 
         return await self._makeApiCall(self.funcinfo["claimNamespace"], *args, **kwargs)
 
-    async def ping(self, *args, **kwargs):
-        """
-        Ping Server
-
-        Respond without doing anything.
-        This endpoint is used to check that the service is up.
-
-        This method is ``stable``
-        """
-
-        return await self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
-
     funcinfo = {
         "claimNamespace": {
             'args': ['namespace'],
-            'input': 'http://schemas.taskcluster.net/pulse/v1/namespace-request.json',
+            'input': 'v1/namespace-request.json#',
             'method': 'post',
             'name': 'claimNamespace',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/namespace-response.json',
+            'output': 'v1/namespace-response.json#',
             'route': '/namespace/<namespace>',
             'stability': 'experimental',
         },
         "listNamespaces": {
             'args': [],
             'method': 'get',
             'name': 'listNamespaces',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/list-namespaces-response.json',
-            'query': ['limit', 'continuation'],
+            'output': 'v1/list-namespaces-response.json#',
+            'query': ['limit', 'continuationToken'],
             'route': '/namespaces',
             'stability': 'experimental',
         },
         "namespace": {
             'args': ['namespace'],
             'method': 'get',
             'name': 'namespace',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/namespace.json',
+            'output': 'v1/namespace.json#',
             'route': '/namespace/<namespace>',
             'stability': 'experimental',
         },
-        "overview": {
-            'args': [],
-            'method': 'get',
-            'name': 'overview',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/rabbit-overview.json',
-            'route': '/overview',
-            'stability': 'experimental',
-        },
         "ping": {
             'args': [],
             'method': 'get',
             'name': 'ping',
             'route': '/ping',
             'stability': 'stable',
         },
     }
--- a/third_party/python/taskcluster/taskcluster/aio/purgecache.py
+++ b/third_party/python/taskcluster/taskcluster/aio/purgecache.py
@@ -8,27 +8,27 @@ from .asyncclient import createApiClient
 from .asyncclient import config
 from .asyncclient import createTemporaryCredentials
 from .asyncclient import createSession
 _defaultConfig = config
 
 
 class PurgeCache(AsyncBaseClient):
     """
-    The purge-cache service, typically available at
-    `purge-cache.taskcluster.net`, is responsible for publishing a pulse
+    The purge-cache service is responsible for publishing a pulse
     message for workers, so they can purge cache upon request.
 
     This document describes the API end-point for publishing the pulse
     message. This is mainly intended to be used by tools.
     """
 
     classOptions = {
-        "baseUrl": "https://purge-cache.taskcluster.net/v1/"
     }
+    serviceName = 'purge-cache'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/purgecacheevents.py
+++ b/third_party/python/taskcluster/taskcluster/aio/purgecacheevents.py
@@ -17,18 +17,20 @@ class PurgeCacheEvents(AsyncBaseClient):
     `purge-cache.taskcluster.net`, is responsible for publishing a pulse
     message for workers, so they can purge cache upon request.
 
     This document describes the exchange offered for workers by the
     cache-purge service.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-purge-cache/v1/"
+        "exchangePrefix": "exchange/taskcluster-purge-cache/v1/",
     }
+    serviceName = 'purge-cache'
+    apiVersion = 'v1'
 
     def purgeCache(self, *args, **kwargs):
         """
         Purge Cache Messages
 
         When a cache purge is requested  a message will be posted on this
         exchange with designated `provisionerId` and `workerType` in the
         routing-key and the name of the `cacheFolder` as payload
--- a/third_party/python/taskcluster/taskcluster/aio/queue.py
+++ b/third_party/python/taskcluster/taskcluster/aio/queue.py
@@ -20,18 +20,19 @@ class Queue(AsyncBaseClient):
     This document describes the API end-points offered by the queue. These
     end-points targets the following audience:
      * Schedulers, who create tasks to be executed,
      * Workers, who execute tasks, and
      * Tools, that wants to inspect the state of a task.
     """
 
     classOptions = {
-        "baseUrl": "https://queue.taskcluster.net/v1/"
     }
+    serviceName = 'queue'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/queueevents.py
+++ b/third_party/python/taskcluster/taskcluster/aio/queueevents.py
@@ -58,18 +58,20 @@ class QueueEvents(AsyncBaseClient):
 
     **Remark**, some message generated by timeouts maybe dropped if the
     server crashes at wrong time. Ideally, we'll address this in the
     future. For now we suggest you ignore this corner case, and notify us
     if this corner case is of concern to you.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-queue/v1/"
+        "exchangePrefix": "exchange/taskcluster-queue/v1/",
     }
+    serviceName = 'queue'
+    apiVersion = 'v1'
 
     def taskDefined(self, *args, **kwargs):
         """
         Task Defined Messages
 
         When a task is created or just defined a message is posted to this
         exchange.
 
--- a/third_party/python/taskcluster/taskcluster/aio/secrets.py
+++ b/third_party/python/taskcluster/taskcluster/aio/secrets.py
@@ -18,18 +18,19 @@ class Secrets(AsyncBaseClient):
     those who do not have the relevant scopes.
 
     Secrets also have an expiration date, and once a secret has expired it can no
     longer be read.  This is useful for short-term secrets such as a temporary
     service credential or a one-time signing key.
     """
 
     classOptions = {
-        "baseUrl": "https://secrets.taskcluster.net/v1/"
     }
+    serviceName = 'secrets'
+    apiVersion = 'v1'
 
     async def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/aio/treeherderevents.py
+++ b/third_party/python/taskcluster/taskcluster/aio/treeherderevents.py
@@ -18,27 +18,29 @@ class TreeherderEvents(AsyncBaseClient):
     that are consumable by Treeherder.
 
     This exchange provides that job messages to be consumed by any queue that
     attached to the exchange.  This could be a production Treeheder instance,
     a local development environment, or a custom dashboard.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-treeherder/v1/"
+        "exchangePrefix": "exchange/taskcluster-treeherder/v1/",
     }
+    serviceName = 'treeherder'
+    apiVersion = 'v1'
 
     def jobs(self, *args, **kwargs):
         """
         Job Messages
 
         When a task run is scheduled or resolved, a message is posted to
         this exchange in a Treeherder consumable format.
 
-        This exchange outputs: ``http://schemas.taskcluster.net/taskcluster-treeherder/v1/pulse-job.json#``This exchange takes the following keys:
+        This exchange outputs: ``v1/pulse-job.json#``This exchange takes the following keys:
 
          * destination: destination (required)
 
          * project: project (required)
 
          * reserved: Space reserved for future routing-key entries, you should always match this entry with `#`. As automatically done by our tooling, if not specified.
         """
 
@@ -54,17 +56,17 @@ class TreeherderEvents(AsyncBaseClient):
                     'multipleWords': False,
                     'name': 'project',
                 },
                 {
                     'multipleWords': True,
                     'name': 'reserved',
                 },
             ],
-            'schema': 'http://schemas.taskcluster.net/taskcluster-treeherder/v1/pulse-job.json#',
+            'schema': 'v1/pulse-job.json#',
         }
         return self._makeTopicExchange(ref, *args, **kwargs)
 
     funcinfo = {
     }
 
 
 __all__ = ['createTemporaryCredentials', 'config', '_defaultConfig', 'createApiClient', 'createSession', 'TreeherderEvents']
--- a/third_party/python/taskcluster/taskcluster/auth.py
+++ b/third_party/python/taskcluster/taskcluster/auth.py
@@ -50,18 +50,19 @@ class Auth(BaseClient):
     The authentication service also has API end-points for delegating access
     to some guarded service such as AWS S3, or Azure Table Storage.
     Generally, we add API end-points to this server when we wish to use
     Taskcluster credentials to grant access to a third-party service used
     by many Taskcluster components.
     """
 
     classOptions = {
-        "baseUrl": "https://auth.taskcluster.net/v1/"
     }
+    serviceName = 'auth'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/authevents.py
+++ b/third_party/python/taskcluster/taskcluster/authevents.py
@@ -8,29 +8,31 @@ from .client import createApiClient
 from .client import config
 from .client import createTemporaryCredentials
 from .client import createSession
 _defaultConfig = config
 
 
 class AuthEvents(BaseClient):
     """
-    The auth service, typically available at `auth.taskcluster.net`
-    is responsible for storing credentials, managing assignment of scopes,
-    and validation of request signatures from other services.
+    The auth service is responsible for storing credentials, managing
+    assignment of scopes, and validation of request signatures from other
+    services.
 
     These exchanges provides notifications when credentials or roles are
     updated. This is mostly so that multiple instances of the auth service
     can purge their caches and synchronize state. But you are of course
     welcome to use these for other purposes, monitoring changes for example.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-auth/v1/"
+        "exchangePrefix": "exchange/taskcluster-auth/v1/",
     }
+    serviceName = 'auth'
+    apiVersion = 'v1'
 
     def clientCreated(self, *args, **kwargs):
         """
         Client Created Messages
 
         Message that a new client has been created.
 
         This exchange outputs: ``v1/client-message.json#``This exchange takes the following keys:
--- a/third_party/python/taskcluster/taskcluster/awsprovisioner.py
+++ b/third_party/python/taskcluster/taskcluster/awsprovisioner.py
@@ -39,18 +39,19 @@ class AwsProvisioner(BaseClient):
     the worker's credentials and any needed passwords or other restricted
     information.  The worker is responsible for deleting the secret after
     retrieving it, to prevent dissemination of the secret to other proceses
     which can read the instance user data.
 
     """
 
     classOptions = {
-        "baseUrl": "https://aws-provisioner.taskcluster.net/v1"
     }
+    serviceName = 'aws-provisioner'
+    apiVersion = 'v1'
 
     def listWorkerTypeSummaries(self, *args, **kwargs):
         """
         List worker types with details
 
         Return a list of worker types, including some summary information about
         current capacity for each.  While this list includes all defined worker types,
         there may be running EC2 instances for deleted worker types that are not
--- a/third_party/python/taskcluster/taskcluster/awsprovisionerevents.py
+++ b/third_party/python/taskcluster/taskcluster/awsprovisionerevents.py
@@ -12,18 +12,19 @@ from .client import createSession
 
 
 class AwsProvisionerEvents(BaseClient):
     """
     Exchanges from the provisioner... more docs later
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-aws-provisioner/v1/"
+        "exchangePrefix": "exchange/taskcluster-aws-provisioner/v1/",
     }
+    apiVersion = 'v1'
 
     def workerTypeCreated(self, *args, **kwargs):
         """
         WorkerType Created Message
 
         When a new `workerType` is created a message will be published to this
         exchange.
 
--- a/third_party/python/taskcluster/taskcluster/client.py
+++ b/third_party/python/taskcluster/taskcluster/client.py
@@ -1,13 +1,12 @@
 """This module is used to interact with taskcluster rest apis"""
 
 from __future__ import absolute_import, division, print_function
 
-import os
 import json
 import logging
 import copy
 import hashlib
 import hmac
 import datetime
 import calendar
 import requests
@@ -16,27 +15,29 @@ import six
 import warnings
 from six.moves import urllib
 
 import mohawk
 import mohawk.bewit
 
 import taskcluster.exceptions as exceptions
 import taskcluster.utils as utils
+import taskcluster_urls as liburls
 
 log = logging.getLogger(__name__)
 
 
 # Default configuration
 _defaultConfig = config = {
     'credentials': {
-        'clientId': os.environ.get('TASKCLUSTER_CLIENT_ID'),
-        'accessToken': os.environ.get('TASKCLUSTER_ACCESS_TOKEN'),
-        'certificate': os.environ.get('TASKCLUSTER_CERTIFICATE'),
+        'clientId': None,
+        'accessToken': None,
+        'certificate': None,
     },
+    'rootUrl': None,
     'maxRetries': 5,
     'signedUrlExpiration': 15 * 60,
 }
 
 
 def createSession(*args, **kwargs):
     """ Create a new requests session.  This passes through all positional and
     keyword arguments to the requests.Session() constructor
@@ -47,31 +48,36 @@ def createSession(*args, **kwargs):
 class BaseClient(object):
     """ Base Class for API Client Classes. Each individual Client class
     needs to set up its own methods for REST endpoints and Topic Exchange
     routing key patterns.  The _makeApiCall() and _topicExchange() methods
     help with this.
     """
 
     def __init__(self, options=None, session=None):
+        if options and options.get('baseUrl'):
+            raise exceptions.TaskclusterFailure('baseUrl option is no longer allowed')
         o = copy.deepcopy(self.classOptions)
         o.update(_defaultConfig)
         if options:
             o.update(options)
+        if not o.get('rootUrl'):
+            raise exceptions.TaskclusterFailure('rootUrl option is required')
 
         credentials = o.get('credentials')
         if credentials:
             for x in ('accessToken', 'clientId', 'certificate'):
                 value = credentials.get(x)
                 if value and not isinstance(value, six.binary_type):
                     try:
                         credentials[x] = credentials[x].encode('ascii')
                     except:
                         s = '%s (%s) must be unicode encodable' % (x, credentials[x])
                         raise exceptions.TaskclusterAuthFailure(s)
+
         self.options = o
         if 'credentials' in o:
             log.debug('credentials key scrubbed from logging output')
         log.debug(dict((k, v) for k, v in o.items() if k != 'credentials'))
 
         if session:
             self.session = session
         else:
@@ -163,17 +169,17 @@ class BaseClient(object):
         entry = self.funcinfo.get(methodName)
         if not entry:
             raise exceptions.TaskclusterFailure(
                 'Requested method "%s" not found in API Reference' % methodName)
         routeParams, _, query, _, _ = self._processArgs(entry, *args, **kwargs)
         route = self._subArgsInRoute(entry, routeParams)
         if query:
             route += '?' + urllib.parse.urlencode(query)
-        return self._joinBaseUrlAndRoute(route)
+        return liburls.api(self.options['rootUrl'], self.serviceName, self.apiVersion, route)
 
     def buildSignedUrl(self, methodName, *args, **kwargs):
         """ Build a signed URL.  This URL contains the credentials needed to access
         a resource."""
 
         if 'expiration' in kwargs:
             expiration = kwargs['expiration']
             del kwargs['expiration']
@@ -232,21 +238,24 @@ class BaseClient(object):
             u.scheme,
             u.netloc,
             u.path,
             u.params,
             qs,
             u.fragment,
         ))
 
-    def _joinBaseUrlAndRoute(self, route):
-        return urllib.parse.urljoin(
-            '{}/'.format(self.options['baseUrl'].rstrip('/')),
-            route.lstrip('/')
-        )
+    def _constructUrl(self, route):
+        """Construct a URL for the given route on this service, based on the
+        rootUrl"""
+        return liburls.api(
+            self.options['rootUrl'],
+            self.serviceName,
+            self.apiVersion,
+            route.rstrip('/'))
 
     def _makeApiCall(self, entry, *args, **kwargs):
         """ This function is used to dispatch calls to other functions
         for a given API Reference entry"""
 
         x = self._processArgs(entry, *args, **kwargs)
         routeParams, payload, query, paginationHandler, paginationLimit = x
         route = self._subArgsInRoute(entry, routeParams)
@@ -432,17 +441,17 @@ class BaseClient(object):
             cred['accessToken']
         )
 
     def _makeHttpRequest(self, method, route, payload):
         """ Make an HTTP Request for the API endpoint.  This method wraps
         the logic about doing failure retry and passes off the actual work
         of doing an HTTP request to another method."""
 
-        url = self._joinBaseUrlAndRoute(route)
+        url = self._constructUrl(route)
         log.debug('Full URL used is: %s', url)
 
         hawkExt = self.makeHawkExt()
 
         # Serialize payload if given
         if payload is not None:
             payload = utils.dumpJson(payload)
 
@@ -540,29 +549,41 @@ class BaseClient(object):
             except ValueError:
                 return {"response": response}
 
         # This code-path should be unreachable
         assert False, "Error from last retry should have been raised!"
 
 
 def createApiClient(name, api):
+    api = api['reference']
+
     attributes = dict(
         name=name,
         __doc__=api.get('description'),
         classOptions={},
         funcinfo={},
     )
 
-    copiedOptions = ('baseUrl', 'exchangePrefix')
+    # apply a default for apiVersion; this can be removed when all services
+    # have apiVersion
+    if 'apiVersion' not in api:
+        api['apiVersion'] = 'v1'
+
+    copiedOptions = ('exchangePrefix',)
     for opt in copiedOptions:
-        if opt in api['reference']:
-            attributes['classOptions'][opt] = api['reference'][opt]
+        if opt in api:
+            attributes['classOptions'][opt] = api[opt]
 
-    for entry in api['reference']['entries']:
+    copiedProperties = ('serviceName', 'apiVersion')
+    for opt in copiedProperties:
+        if opt in api:
+            attributes[opt] = api[opt]
+
+    for entry in api['entries']:
         if entry['type'] == 'function':
             def addApiCall(e):
                 def apiCall(self, *args, **kwargs):
                     return self._makeApiCall(e, *args, **kwargs)
                 return apiCall
             f = addApiCall(entry)
 
             docStr = "Call the %s api's %s method.  " % (name, entry['name'])
--- a/third_party/python/taskcluster/taskcluster/ec2manager.py
+++ b/third_party/python/taskcluster/taskcluster/ec2manager.py
@@ -12,39 +12,52 @@ from .client import createSession
 
 
 class EC2Manager(BaseClient):
     """
     A taskcluster service which manages EC2 instances.  This service does not understand any taskcluster concepts intrinsicaly other than using the name `workerType` to refer to a group of associated instances.  Unless you are working on building a provisioner for AWS, you almost certainly do not want to use this service
     """
 
     classOptions = {
-        "baseUrl": "https://ec2-manager.taskcluster.net/v1"
     }
+    serviceName = 'ec2-manager'
+    apiVersion = 'v1'
+
+    def ping(self, *args, **kwargs):
+        """
+        Ping Server
+
+        Respond without doing anything.
+        This endpoint is used to check that the service is up.
+
+        This method is ``stable``
+        """
+
+        return self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
 
     def listWorkerTypes(self, *args, **kwargs):
         """
         See the list of worker types which are known to be managed
 
         This method is only for debugging the ec2-manager
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/list-worker-types.json#``
+        This method gives output: ``v1/list-worker-types.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["listWorkerTypes"], *args, **kwargs)
 
     def runInstance(self, *args, **kwargs):
         """
         Run an instance
 
         Request an instance of a worker type
 
-        This method takes input: ``http://schemas.taskcluster.net/ec2-manager/v1/run-instance-request.json#``
+        This method takes input: ``v1/run-instance-request.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["runInstance"], *args, **kwargs)
 
     def terminateWorkerType(self, *args, **kwargs):
         """
@@ -58,69 +71,69 @@ class EC2Manager(BaseClient):
         return self._makeApiCall(self.funcinfo["terminateWorkerType"], *args, **kwargs)
 
     def workerTypeStats(self, *args, **kwargs):
         """
         Look up the resource stats for a workerType
 
         Return an object which has a generic state description. This only contains counts of instances
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/worker-type-resources.json#``
+        This method gives output: ``v1/worker-type-resources.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["workerTypeStats"], *args, **kwargs)
 
     def workerTypeHealth(self, *args, **kwargs):
         """
         Look up the resource health for a workerType
 
         Return a view of the health of a given worker type
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/health.json#``
+        This method gives output: ``v1/health.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["workerTypeHealth"], *args, **kwargs)
 
     def workerTypeErrors(self, *args, **kwargs):
         """
         Look up the most recent errors of a workerType
 
         Return a list of the most recent errors encountered by a worker type
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/errors.json#``
+        This method gives output: ``v1/errors.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["workerTypeErrors"], *args, **kwargs)
 
     def workerTypeState(self, *args, **kwargs):
         """
         Look up the resource state for a workerType
 
         Return state information for a given worker type
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/worker-type-state.json#``
+        This method gives output: ``v1/worker-type-state.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["workerTypeState"], *args, **kwargs)
 
     def ensureKeyPair(self, *args, **kwargs):
         """
         Ensure a KeyPair for a given worker type exists
 
         Idempotently ensure that a keypair of a given name exists
 
-        This method takes input: ``http://schemas.taskcluster.net/ec2-manager/v1/create-key-pair.json#``
+        This method takes input: ``v1/create-key-pair.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["ensureKeyPair"], *args, **kwargs)
 
     def removeKeyPair(self, *args, **kwargs):
         """
@@ -145,58 +158,58 @@ class EC2Manager(BaseClient):
         return self._makeApiCall(self.funcinfo["terminateInstance"], *args, **kwargs)
 
     def getPrices(self, *args, **kwargs):
         """
         Request prices for EC2
 
         Return a list of possible prices for EC2
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/prices.json#``
+        This method gives output: ``v1/prices.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["getPrices"], *args, **kwargs)
 
     def getSpecificPrices(self, *args, **kwargs):
         """
         Request prices for EC2
 
         Return a list of possible prices for EC2
 
-        This method takes input: ``http://schemas.taskcluster.net/ec2-manager/v1/prices-request.json#``
+        This method takes input: ``v1/prices-request.json#``
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/prices.json#``
+        This method gives output: ``v1/prices.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["getSpecificPrices"], *args, **kwargs)
 
     def getHealth(self, *args, **kwargs):
         """
         Get EC2 account health metrics
 
         Give some basic stats on the health of our EC2 account
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/health.json#``
+        This method gives output: ``v1/health.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["getHealth"], *args, **kwargs)
 
     def getRecentErrors(self, *args, **kwargs):
         """
         Look up the most recent errors in the provisioner across all worker types
 
         Return a list of recent errors encountered
 
-        This method gives output: ``http://schemas.taskcluster.net/ec2-manager/v1/errors.json#``
+        This method gives output: ``v1/errors.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["getRecentErrors"], *args, **kwargs)
 
     def regions(self, *args, **kwargs):
         """
@@ -284,121 +297,91 @@ class EC2Manager(BaseClient):
 
         This method is only for debugging the ec2-manager
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["purgeQueues"], *args, **kwargs)
 
-    def apiReference(self, *args, **kwargs):
-        """
-        API Reference
-
-        Generate an API reference for this service
-
-        This method is ``experimental``
-        """
-
-        return self._makeApiCall(self.funcinfo["apiReference"], *args, **kwargs)
-
-    def ping(self, *args, **kwargs):
-        """
-        Ping Server
-
-        Respond without doing anything.
-        This endpoint is used to check that the service is up.
-
-        This method is ``stable``
-        """
-
-        return self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
-
     funcinfo = {
         "allState": {
             'args': [],
             'method': 'get',
             'name': 'allState',
             'route': '/internal/all-state',
             'stability': 'experimental',
         },
         "amiUsage": {
             'args': [],
             'method': 'get',
             'name': 'amiUsage',
             'route': '/internal/ami-usage',
             'stability': 'experimental',
         },
-        "apiReference": {
-            'args': [],
-            'method': 'get',
-            'name': 'apiReference',
-            'route': '/internal/api-reference',
-            'stability': 'experimental',
-        },
         "dbpoolStats": {
             'args': [],
             'method': 'get',
             'name': 'dbpoolStats',
             'route': '/internal/db-pool-stats',
             'stability': 'experimental',
         },
         "ebsUsage": {
             'args': [],
             'method': 'get',
             'name': 'ebsUsage',
             'route': '/internal/ebs-usage',
             'stability': 'experimental',
         },
         "ensureKeyPair": {
             'args': ['name'],
-            'input': 'http://schemas.taskcluster.net/ec2-manager/v1/create-key-pair.json#',
+            'input': 'v1/create-key-pair.json#',
             'method': 'get',
             'name': 'ensureKeyPair',
             'route': '/key-pairs/<name>',
             'stability': 'experimental',
         },
         "getHealth": {
             'args': [],
             'method': 'get',
             'name': 'getHealth',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/health.json#',
+            'output': 'v1/health.json#',
             'route': '/health',
             'stability': 'experimental',
         },
         "getPrices": {
             'args': [],
             'method': 'get',
             'name': 'getPrices',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/prices.json#',
+            'output': 'v1/prices.json#',
             'route': '/prices',
             'stability': 'experimental',
         },
         "getRecentErrors": {
             'args': [],
             'method': 'get',
             'name': 'getRecentErrors',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/errors.json#',
+            'output': 'v1/errors.json#',
             'route': '/errors',
             'stability': 'experimental',
         },
         "getSpecificPrices": {
             'args': [],
-            'input': 'http://schemas.taskcluster.net/ec2-manager/v1/prices-request.json#',
+            'input': 'v1/prices-request.json#',
             'method': 'post',
             'name': 'getSpecificPrices',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/prices.json#',
+            'output': 'v1/prices.json#',
             'route': '/prices',
             'stability': 'experimental',
         },
         "listWorkerTypes": {
             'args': [],
             'method': 'get',
             'name': 'listWorkerTypes',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/list-worker-types.json#',
+            'output': 'v1/list-worker-types.json#',
             'route': '/worker-types',
             'stability': 'experimental',
         },
         "ping": {
             'args': [],
             'method': 'get',
             'name': 'ping',
             'route': '/ping',
@@ -422,17 +405,17 @@ class EC2Manager(BaseClient):
             'args': ['name'],
             'method': 'delete',
             'name': 'removeKeyPair',
             'route': '/key-pairs/<name>',
             'stability': 'experimental',
         },
         "runInstance": {
             'args': ['workerType'],
-            'input': 'http://schemas.taskcluster.net/ec2-manager/v1/run-instance-request.json#',
+            'input': 'v1/run-instance-request.json#',
             'method': 'put',
             'name': 'runInstance',
             'route': '/worker-types/<workerType>/instance',
             'stability': 'experimental',
         },
         "sqsStats": {
             'args': [],
             'method': 'get',
@@ -453,40 +436,40 @@ class EC2Manager(BaseClient):
             'name': 'terminateWorkerType',
             'route': '/worker-types/<workerType>/resources',
             'stability': 'experimental',
         },
         "workerTypeErrors": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeErrors',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/errors.json#',
+            'output': 'v1/errors.json#',
             'route': '/worker-types/<workerType>/errors',
             'stability': 'experimental',
         },
         "workerTypeHealth": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeHealth',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/health.json#',
+            'output': 'v1/health.json#',
             'route': '/worker-types/<workerType>/health',
             'stability': 'experimental',
         },
         "workerTypeState": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeState',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/worker-type-state.json#',
+            'output': 'v1/worker-type-state.json#',
             'route': '/worker-types/<workerType>/state',
             'stability': 'experimental',
         },
         "workerTypeStats": {
             'args': ['workerType'],
             'method': 'get',
             'name': 'workerTypeStats',
-            'output': 'http://schemas.taskcluster.net/ec2-manager/v1/worker-type-resources.json#',
+            'output': 'v1/worker-type-resources.json#',
             'route': '/worker-types/<workerType>/stats',
             'stability': 'experimental',
         },
     }
 
 
 __all__ = ['createTemporaryCredentials', 'config', '_defaultConfig', 'createApiClient', 'createSession', 'EC2Manager']
--- a/third_party/python/taskcluster/taskcluster/github.py
+++ b/third_party/python/taskcluster/taskcluster/github.py
@@ -8,30 +8,30 @@ from .client import createApiClient
 from .client import config
 from .client import createTemporaryCredentials
 from .client import createSession
 _defaultConfig = config
 
 
 class Github(BaseClient):
     """
-    The github service, typically available at
-    `github.taskcluster.net`, is responsible for publishing pulse
-    messages in response to GitHub events.
+    The github service is responsible for creating tasks in reposnse
+    to GitHub events, and posting results to the GitHub UI.
 
     This document describes the API end-point for consuming GitHub
     web hooks, as well as some useful consumer APIs.
 
     When Github forbids an action, this service returns an HTTP 403
     with code ForbiddenByGithub.
     """
 
     classOptions = {
-        "baseUrl": "https://github.taskcluster.net/v1/"
     }
+    serviceName = 'github'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/githubevents.py
+++ b/third_party/python/taskcluster/taskcluster/githubevents.py
@@ -17,18 +17,20 @@ class GithubEvents(BaseClient):
     message for supported github events, translating Github webhook
     events into pulse messages.
 
     This document describes the exchange offered by the taskcluster
     github service
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-github/v1/"
+        "exchangePrefix": "exchange/taskcluster-github/v1/",
     }
+    serviceName = 'github'
+    apiVersion = 'v1'
 
     def pullRequest(self, *args, **kwargs):
         """
         GitHub Pull Request Event
 
         When a GitHub pull request event is posted it will be broadcast on this
         exchange with the designated `organization` and `repository`
         in the routing-key along with event specific metadata in the payload.
@@ -143,13 +145,50 @@ class GithubEvents(BaseClient):
                     'multipleWords': False,
                     'name': 'repository',
                 },
             ],
             'schema': 'v1/github-release-message.json#',
         }
         return self._makeTopicExchange(ref, *args, **kwargs)
 
+    def taskGroupDefined(self, *args, **kwargs):
+        """
+        GitHub release Event
+
+        used for creating status indicators in GitHub UI using Statuses API
+
+        This exchange outputs: ``v1/task-group-defined-message.json#``This exchange takes the following keys:
+
+         * routingKeyKind: Identifier for the routing-key kind. This is always `"primary"` for the formalized routing key. (required)
+
+         * organization: The GitHub `organization` which had an event. All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped. (required)
+
+         * repository: The GitHub `repository` which had an event.All periods have been replaced by % - such that foo.bar becomes foo%bar - and all other special characters aside from - and _ have been stripped. (required)
+        """
+
+        ref = {
+            'exchange': 'task-group-defined',
+            'name': 'taskGroupDefined',
+            'routingKey': [
+                {
+                    'constant': 'primary',
+                    'multipleWords': False,
+                    'name': 'routingKeyKind',
+                },
+                {
+                    'multipleWords': False,
+                    'name': 'organization',
+                },
+                {
+                    'multipleWords': False,
+                    'name': 'repository',
+                },
+            ],
+            'schema': 'v1/task-group-defined-message.json#',
+        }
+        return self._makeTopicExchange(ref, *args, **kwargs)
+
     funcinfo = {
     }
 
 
 __all__ = ['createTemporaryCredentials', 'config', '_defaultConfig', 'createApiClient', 'createSession', 'GithubEvents']
--- a/third_party/python/taskcluster/taskcluster/hooks.py
+++ b/third_party/python/taskcluster/taskcluster/hooks.py
@@ -25,23 +25,24 @@ class Hooks(BaseClient):
 
     Hooks can have a "schedule" indicating specific times that new tasks should
     be created.  Each schedule is in a simple cron format, per
     https://www.npmjs.com/package/cron-parser.  For example:
      * `['0 0 1 * * *']` -- daily at 1:00 UTC
      * `['0 0 9,21 * * 1-5', '0 0 12 * * 0,6']` -- weekdays at 9:00 and 21:00 UTC, weekends at noon
 
     The task definition is used as a JSON-e template, with a context depending on how it is fired.  See
-    https://docs.taskcluster.net/reference/core/taskcluster-hooks/docs/firing-hooks
+    [/docs/reference/core/taskcluster-hooks/docs/firing-hooks](firing-hooks)
     for more information.
     """
 
     classOptions = {
-        "baseUrl": "https://hooks.taskcluster.net/v1/"
     }
+    serviceName = 'hooks'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/index.py
+++ b/third_party/python/taskcluster/taskcluster/index.py
@@ -103,18 +103,19 @@ class Index(BaseClient):
     to listen for messages about these tasks. For
     example one could bind to `route.index.some-app.*.release-build`,
     and pick up all messages about release builds. Hence, it is a
     good idea to document task index hierarchies, as these make up extension
     points in their own.
     """
 
     classOptions = {
-        "baseUrl": "https://index.taskcluster.net/v1/"
     }
+    serviceName = 'index'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/login.py
+++ b/third_party/python/taskcluster/taskcluster/login.py
@@ -13,18 +13,31 @@ from .client import createSession
 
 class Login(BaseClient):
     """
     The Login service serves as the interface between external authentication
     systems and Taskcluster credentials.
     """
 
     classOptions = {
-        "baseUrl": "https://login.taskcluster.net/v1"
     }
+    serviceName = 'login'
+    apiVersion = 'v1'
+
+    def ping(self, *args, **kwargs):
+        """
+        Ping Server
+
+        Respond without doing anything.
+        This endpoint is used to check that the service is up.
+
+        This method is ``stable``
+        """
+
+        return self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
 
     def oidcCredentials(self, *args, **kwargs):
         """
         Get Taskcluster credentials given a suitable `access_token`
 
         Given an OIDC `access_token` from a trusted OpenID provider, return a
         set of Taskcluster credentials for use on behalf of the identified
         user.
@@ -32,51 +45,39 @@ class Login(BaseClient):
         This method is typically not called with a Taskcluster client library
         and does not accept Hawk credentials. The `access_token` should be
         given in an `Authorization` header:
         ```
         Authorization: Bearer abc.xyz
         ```
 
         The `access_token` is first verified against the named
-        :provider, then passed to the provider's API to retrieve a user
+        :provider, then passed to the provider's APIBuilder to retrieve a user
         profile. That profile is then used to generate Taskcluster credentials
         appropriate to the user. Note that the resulting credentials may or may
         not include a `certificate` property. Callers should be prepared for either
         alternative.
 
         The given credentials will expire in a relatively short time. Callers should
         monitor this expiration and refresh the credentials if necessary, by calling
         this endpoint again, if they have expired.
 
-        This method gives output: ``http://schemas.taskcluster.net/login/v1/oidc-credentials-response.json``
+        This method gives output: ``v1/oidc-credentials-response.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["oidcCredentials"], *args, **kwargs)
 
-    def ping(self, *args, **kwargs):
-        """
-        Ping Server
-
-        Respond without doing anything.
-        This endpoint is used to check that the service is up.
-
-        This method is ``stable``
-        """
-
-        return self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
-
     funcinfo = {
         "oidcCredentials": {
             'args': ['provider'],
             'method': 'get',
             'name': 'oidcCredentials',
-            'output': 'http://schemas.taskcluster.net/login/v1/oidc-credentials-response.json',
+            'output': 'v1/oidc-credentials-response.json#',
             'route': '/oidc-credentials/<provider>',
             'stability': 'experimental',
         },
         "ping": {
             'args': [],
             'method': 'get',
             'name': 'ping',
             'route': '/ping',
--- a/third_party/python/taskcluster/taskcluster/notify.py
+++ b/third_party/python/taskcluster/taskcluster/notify.py
@@ -14,18 +14,19 @@ from .client import createSession
 class Notify(BaseClient):
     """
     The notification service, typically available at `notify.taskcluster.net`
     listens for tasks with associated notifications and handles requests to
     send emails and post pulse messages.
     """
 
     classOptions = {
-        "baseUrl": "https://notify.taskcluster.net/v1/"
     }
+    serviceName = 'notify'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/pulse.py
+++ b/third_party/python/taskcluster/taskcluster/pulse.py
@@ -17,132 +17,116 @@ class Pulse(BaseClient):
     manages pulse credentials for taskcluster users.
 
     A service to manage Pulse credentials for anything using
     Taskcluster credentials. This allows for self-service pulse
     access and greater control within the Taskcluster project.
     """
 
     classOptions = {
-        "baseUrl": "https://pulse.taskcluster.net/v1"
     }
+    serviceName = 'pulse'
+    apiVersion = 'v1'
 
-    def overview(self, *args, **kwargs):
+    def ping(self, *args, **kwargs):
         """
-        Rabbit Overview
-
-        Get an overview of the Rabbit cluster.
+        Ping Server
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/rabbit-overview.json``
+        Respond without doing anything.
+        This endpoint is used to check that the service is up.
 
-        This method is ``experimental``
+        This method is ``stable``
         """
 
-        return self._makeApiCall(self.funcinfo["overview"], *args, **kwargs)
+        return self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
 
     def listNamespaces(self, *args, **kwargs):
         """
         List Namespaces
 
         List the namespaces managed by this service.
 
         This will list up to 1000 namespaces. If more namespaces are present a
         `continuationToken` will be returned, which can be given in the next
-        request. For the initial request, do not provide continuation.
+        request. For the initial request, do not provide continuation token.
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/list-namespaces-response.json``
+        This method gives output: ``v1/list-namespaces-response.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["listNamespaces"], *args, **kwargs)
 
     def namespace(self, *args, **kwargs):
         """
         Get a namespace
 
         Get public information about a single namespace. This is the same information
         as returned by `listNamespaces`.
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/namespace.json``
+        This method gives output: ``v1/namespace.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["namespace"], *args, **kwargs)
 
     def claimNamespace(self, *args, **kwargs):
         """
         Claim a namespace
 
-        Claim a namespace, returning a username and password with access to that
-        namespace good for a short time.  Clients should call this endpoint again
-        at the re-claim time given in the response, as the password will be rotated
-        soon after that time.  The namespace will expire, and any associated queues
-        and exchanges will be deleted, at the given expiration time.
+        Claim a namespace, returning a connection string with access to that namespace
+        good for use until the `reclaimAt` time in the response body. The connection
+        string can be used as many times as desired during this period, but must not
+        be used after `reclaimAt`.
 
-        The `expires` and `contact` properties can be updated at any time in a reclaim
-        operation.
+        Connections made with this connection string may persist beyond `reclaimAt`,
+        although it should not persist forever.  24 hours is a good maximum, and this
+        service will terminate connections after 72 hours (although this value is
+        configurable).
 
-        This method takes input: ``http://schemas.taskcluster.net/pulse/v1/namespace-request.json``
+        The specified `expires` time updates any existing expiration times.  Connections
+        for expired namespaces will be terminated.
 
-        This method gives output: ``http://schemas.taskcluster.net/pulse/v1/namespace-response.json``
+        This method takes input: ``v1/namespace-request.json#``
+
+        This method gives output: ``v1/namespace-response.json#``
 
         This method is ``experimental``
         """
 
         return self._makeApiCall(self.funcinfo["claimNamespace"], *args, **kwargs)
 
-    def ping(self, *args, **kwargs):
-        """
-        Ping Server
-
-        Respond without doing anything.
-        This endpoint is used to check that the service is up.
-
-        This method is ``stable``
-        """
-
-        return self._makeApiCall(self.funcinfo["ping"], *args, **kwargs)
-
     funcinfo = {
         "claimNamespace": {
             'args': ['namespace'],
-            'input': 'http://schemas.taskcluster.net/pulse/v1/namespace-request.json',
+            'input': 'v1/namespace-request.json#',
             'method': 'post',
             'name': 'claimNamespace',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/namespace-response.json',
+            'output': 'v1/namespace-response.json#',
             'route': '/namespace/<namespace>',
             'stability': 'experimental',
         },
         "listNamespaces": {
             'args': [],
             'method': 'get',
             'name': 'listNamespaces',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/list-namespaces-response.json',
-            'query': ['limit', 'continuation'],
+            'output': 'v1/list-namespaces-response.json#',
+            'query': ['limit', 'continuationToken'],
             'route': '/namespaces',
             'stability': 'experimental',
         },
         "namespace": {
             'args': ['namespace'],
             'method': 'get',
             'name': 'namespace',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/namespace.json',
+            'output': 'v1/namespace.json#',
             'route': '/namespace/<namespace>',
             'stability': 'experimental',
         },
-        "overview": {
-            'args': [],
-            'method': 'get',
-            'name': 'overview',
-            'output': 'http://schemas.taskcluster.net/pulse/v1/rabbit-overview.json',
-            'route': '/overview',
-            'stability': 'experimental',
-        },
         "ping": {
             'args': [],
             'method': 'get',
             'name': 'ping',
             'route': '/ping',
             'stability': 'stable',
         },
     }
--- a/third_party/python/taskcluster/taskcluster/purgecache.py
+++ b/third_party/python/taskcluster/taskcluster/purgecache.py
@@ -8,27 +8,27 @@ from .client import createApiClient
 from .client import config
 from .client import createTemporaryCredentials
 from .client import createSession
 _defaultConfig = config
 
 
 class PurgeCache(BaseClient):
     """
-    The purge-cache service, typically available at
-    `purge-cache.taskcluster.net`, is responsible for publishing a pulse
+    The purge-cache service is responsible for publishing a pulse
     message for workers, so they can purge cache upon request.
 
     This document describes the API end-point for publishing the pulse
     message. This is mainly intended to be used by tools.
     """
 
     classOptions = {
-        "baseUrl": "https://purge-cache.taskcluster.net/v1/"
     }
+    serviceName = 'purge-cache'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/purgecacheevents.py
+++ b/third_party/python/taskcluster/taskcluster/purgecacheevents.py
@@ -17,18 +17,20 @@ class PurgeCacheEvents(BaseClient):
     `purge-cache.taskcluster.net`, is responsible for publishing a pulse
     message for workers, so they can purge cache upon request.
 
     This document describes the exchange offered for workers by the
     cache-purge service.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-purge-cache/v1/"
+        "exchangePrefix": "exchange/taskcluster-purge-cache/v1/",
     }
+    serviceName = 'purge-cache'
+    apiVersion = 'v1'
 
     def purgeCache(self, *args, **kwargs):
         """
         Purge Cache Messages
 
         When a cache purge is requested  a message will be posted on this
         exchange with designated `provisionerId` and `workerType` in the
         routing-key and the name of the `cacheFolder` as payload
--- a/third_party/python/taskcluster/taskcluster/queue.py
+++ b/third_party/python/taskcluster/taskcluster/queue.py
@@ -20,18 +20,19 @@ class Queue(BaseClient):
     This document describes the API end-points offered by the queue. These
     end-points targets the following audience:
      * Schedulers, who create tasks to be executed,
      * Workers, who execute tasks, and
      * Tools, that wants to inspect the state of a task.
     """
 
     classOptions = {
-        "baseUrl": "https://queue.taskcluster.net/v1/"
     }
+    serviceName = 'queue'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/queueevents.py
+++ b/third_party/python/taskcluster/taskcluster/queueevents.py
@@ -58,18 +58,20 @@ class QueueEvents(BaseClient):
 
     **Remark**, some message generated by timeouts maybe dropped if the
     server crashes at wrong time. Ideally, we'll address this in the
     future. For now we suggest you ignore this corner case, and notify us
     if this corner case is of concern to you.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-queue/v1/"
+        "exchangePrefix": "exchange/taskcluster-queue/v1/",
     }
+    serviceName = 'queue'
+    apiVersion = 'v1'
 
     def taskDefined(self, *args, **kwargs):
         """
         Task Defined Messages
 
         When a task is created or just defined a message is posted to this
         exchange.
 
--- a/third_party/python/taskcluster/taskcluster/secrets.py
+++ b/third_party/python/taskcluster/taskcluster/secrets.py
@@ -18,18 +18,19 @@ class Secrets(BaseClient):
     those who do not have the relevant scopes.
 
     Secrets also have an expiration date, and once a secret has expired it can no
     longer be read.  This is useful for short-term secrets such as a temporary
     service credential or a one-time signing key.
     """
 
     classOptions = {
-        "baseUrl": "https://secrets.taskcluster.net/v1/"
     }
+    serviceName = 'secrets'
+    apiVersion = 'v1'
 
     def ping(self, *args, **kwargs):
         """
         Ping Server
 
         Respond without doing anything.
         This endpoint is used to check that the service is up.
 
--- a/third_party/python/taskcluster/taskcluster/treeherderevents.py
+++ b/third_party/python/taskcluster/taskcluster/treeherderevents.py
@@ -18,27 +18,29 @@ class TreeherderEvents(BaseClient):
     that are consumable by Treeherder.
 
     This exchange provides that job messages to be consumed by any queue that
     attached to the exchange.  This could be a production Treeheder instance,
     a local development environment, or a custom dashboard.
     """
 
     classOptions = {
-        "exchangePrefix": "exchange/taskcluster-treeherder/v1/"
+        "exchangePrefix": "exchange/taskcluster-treeherder/v1/",
     }
+    serviceName = 'treeherder'
+    apiVersion = 'v1'
 
     def jobs(self, *args, **kwargs):
         """
         Job Messages
 
         When a task run is scheduled or resolved, a message is posted to
         this exchange in a Treeherder consumable format.
 
-        This exchange outputs: ``http://schemas.taskcluster.net/taskcluster-treeherder/v1/pulse-job.json#``This exchange takes the following keys:
+        This exchange outputs: ``v1/pulse-job.json#``This exchange takes the following keys:
 
          * destination: destination (required)
 
          * project: project (required)
 
          * reserved: Space reserved for future routing-key entries, you should always match this entry with `#`. As automatically done by our tooling, if not specified.
         """
 
@@ -54,17 +56,17 @@ class TreeherderEvents(BaseClient):
                     'multipleWords': False,
                     'name': 'project',
                 },
                 {
                     'multipleWords': True,
                     'name': 'reserved',
                 },
             ],
-            'schema': 'http://schemas.taskcluster.net/taskcluster-treeherder/v1/pulse-job.json#',
+            'schema': 'v1/pulse-job.json#',
         }
         return self._makeTopicExchange(ref, *args, **kwargs)
 
     funcinfo = {
     }
 
 
 __all__ = ['createTemporaryCredentials', 'config', '_defaultConfig', 'createApiClient', 'createSession', 'TreeherderEvents']
--- a/third_party/python/taskcluster/taskcluster/utils.py
+++ b/third_party/python/taskcluster/taskcluster/utils.py
@@ -1,21 +1,21 @@
+# -*- coding: UTF-8 -*-
 from __future__ import absolute_import, division, print_function
 import re
 import json
 import datetime
 import base64
 import logging
 import os
 import requests
 import requests.exceptions
 import slugid
 import time
 import six
-import sys
 import random
 
 from . import exceptions
 
 MAX_RETRIES = 5
 
 DELAY_FACTOR = 0.1
 RANDOMIZATION_FACTOR = 0.25
@@ -293,17 +293,17 @@ def makeSingleHttpRequest(method, url, p
 
     return response
 
 
 def putFile(filename, url, contentType):
     with open(filename, 'rb') as f:
         contentLength = os.fstat(f.fileno()).st_size
         return makeHttpRequest('put', url, f, headers={
-            'Content-Length': contentLength,
+            'Content-Length': str(contentLength),
             'Content-Type': contentType,
         })
 
 
 def encryptEnvVar(taskId, startTime, endTime, name, value, keyFile):
     raise Exception("Encrypted environment variables are no longer supported")
 
 
@@ -314,94 +314,35 @@ def decryptMessage(message, privateKey):
 def isExpired(certificate):
     """ Check if certificate is expired """
     if isinstance(certificate, six.string_types):
         certificate = json.loads(certificate)
     expiry = certificate.get('expiry', 0)
     return expiry < int(time.time() * 1000) + 20 * 60
 
 
-def authenticate(description=None):
-    """
-    Open a web-browser to login.taskcluster.net and listen on localhost for
-    a callback with credentials in query-string.
-
-    The description will be shown on login.taskcluster.net, if not provided
-    a default message with script path will be displayed.
-    """
-    # Importing here to avoid loading these 'obscure' module before it's needed.
-    # Most clients won't use this feature, so we don't want issues with these
-    # modules to affect the library. Maybe they don't work in some environments
-    import webbrowser
-    from six.moves import urllib
-    from six.moves.urllib.parse import quote
-    import BaseHTTPServer
+def optionsFromEnvironment(defaults=None):
+    """Fetch root URL and credentials from the standard TASKCLUSTER_…
+    environment variables and return them in a format suitable for passing to a
+    client constructor."""
+    options = defaults or {}
+    credentials = options.get('credentials', {})
 
-    if not description:
-        script = '[interpreter/unknown]'
-        main = sys.modules.get('__main__', None)
-        if main and hasattr(main, '__file__'):
-            script = os.path.abspath(main.__file__)
-        description = (
-            "Python script: `%s`\n\nWould like some temporary credentials."
-            % script
-        )
-
-    creds = [None]
-
-    class AuthCallBackRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
-        def log_message(format, *args):
-            pass
+    rootUrl = os.environ.get('TASKCLUSTER_ROOT_URL')
+    if rootUrl:
+        options['rootUrl'] = rootUrl
 
-        def do_GET(self):
-            url = urllib.parse.urlparse(self.path)
-            query = urllib.parse.parse_qs(url.query)
-            clientId = query.get('clientId', [None])[0]
-            accessToken = query.get('accessToken', [None])[0]
-            certificate = query.get('certificate', [None])[0]
-            hasCreds = clientId and accessToken and certificate
-            if hasCreds:
-                creds[0] = {
-                    "clientId": clientId,
-                    "accessToken": accessToken,
-                    "certificate": certificate
-                }
-            self.send_response(200)
-            self.send_header('Content-type', 'text/html')
-            self.end_headers()
-            if hasCreds:
-                self.wfile.write("""
-                    <h1>Credentials transferred successfully</h1>
-                    <i>You can close this window now.</i>
-                    <script>window.close();</script>
-                """)
-            else:
-                self.wfile.write("""
-                    <h1>Transfer of credentials failed!</h1>
-                    <p>Something went wrong, you can navigate back and try again...</p>
-                """)
-            return
+    clientId = os.environ.get('TASKCLUSTER_CLIENT_ID')
+    if clientId:
+        credentials['clientId'] = clientId
+
+    accessToken = os.environ.get('TASKCLUSTER_ACCESS_TOKEN')
+    if accessToken:
+        credentials['accessToken'] = accessToken
 
-    # Create server on localhost at random port
-    retries = 5
-    while retries > 0:
-        try:
-            server = BaseHTTPServer.HTTPServer(('', 0), AuthCallBackRequestHandler)
-        except:
-            retries -= 1
-        break
-    port = server.server_address[1]
-
-    query = "?target=" + quote('http://localhost:' + str(port), '')
-    query += "&description=" + quote(description, '')
+    certificate = os.environ.get('TASKCLUSTER_CERTIFICATE')
+    if certificate:
+        credentials['certificate'] = certificate
 
-    webbrowser.open('https://login.taskcluster.net' + query, 1, True)
-    print("")
-    print("-------------------------------------------------------")
-    print("  Opening browser window to login.taskcluster.net")
-    print("  Asking you to grant temporary credentials to:")
-    print("     http://localhost:" + str(port))
-    print("-------------------------------------------------------")
-    print("")
+    if credentials:
+        options['credentials'] = credentials
 
-    while not creds[0]:
-        server.handle_request()
-    return creds[0]
+    return options
--- a/third_party/python/taskcluster/test/test_async.py
+++ b/third_party/python/taskcluster/test/test_async.py
@@ -16,16 +16,17 @@ class TestAuthenticationAsync(base.TCTes
         """we can call methods which require authentication with valid
         permacreds"""
 
         loop = asyncio.get_event_loop()
 
         async def x():
             async with subjectAsync.createSession(loop=loop) as session:
                 client = subjectAsync.Auth({
+                    'rootUrl': self.real_root_url,
                     'credentials': {
                         'clientId': 'tester',
                         'accessToken': 'no-secret',
                     },
                 }, session=session)
                 result = await client.testAuthenticate({
                     'clientScopes': ['test:a'],
                     'requiredScopes': ['test:a'],
@@ -44,18 +45,19 @@ class TestAuthenticationAsync(base.TCTes
                 tempCred = subjectAsync.createTemporaryCredentials(
                     'tester',
                     'no-secret',
                     datetime.datetime.utcnow(),
                     datetime.datetime.utcnow() + datetime.timedelta(hours=1),
                     ['test:xyz'],
                 )
                 client = subjectAsync.Auth({
+                    'rootUrl': self.real_root_url,
                     'credentials': tempCred,
                 }, session=session)
 
-                result = client.testAuthenticate({
+                result = await client.testAuthenticate({
                     'clientScopes': ['test:*'],
                     'requiredScopes': ['test:xyz'],
                 })
                 self.assertEqual(result, {'scopes': ['test:xyz'], 'clientId': 'tester'})
 
-        loop.run_until_complete
+        loop.run_until_complete(x())
--- a/third_party/python/taskcluster/test/test_client.py
+++ b/third_party/python/taskcluster/test/test_client.py
@@ -2,25 +2,27 @@ from __future__ import division, print_f
 import types
 import unittest
 import time
 import datetime
 from six.moves import urllib
 import os
 import re
 import json
+import copy
 
 import mock
 import httmock
 import requests
 
 import base
 import taskcluster.auth as subject
 import taskcluster.exceptions as exc
 import taskcluster.utils as utils
+import taskcluster_urls as liburls
 
 
 class ClientTest(base.TCTest):
 
     realTimeSleep = time.sleep
 
     def setUp(self):
         subject.config['credentials'] = {
@@ -40,27 +42,53 @@ class ClientTest(base.TCTest):
             base.createApiEntryFunction('two_args_no_input', 2, False),
             base.createApiEntryFunction('no_args_with_input', 0, True),
             base.createApiEntryFunction('two_args_with_input', 2, True),
             base.createApiEntryFunction('NEVER_CALL_ME', 0, False),
             topicEntry
         ]
         self.apiRef = base.createApiRef(entries=entries)
         self.clientClass = subject.createApiClient('testApi', self.apiRef)
-        self.client = self.clientClass()
+        self.client = self.clientClass({'rootUrl': self.test_root_url})
         # Patch time.sleep so that we don't delay tests
         sleepPatcher = mock.patch('time.sleep')
         sleepSleep = sleepPatcher.start()
         sleepSleep.return_value = None
         self.addCleanup(sleepSleep.stop)
 
     def tearDown(self):
         time.sleep = self.realTimeSleep
 
 
+class TestConstructorOptions(ClientTest):
+
+    def test_baseUrl_not_allowed(self):
+        with self.assertRaises(exc.TaskclusterFailure):
+            self.clientClass({'baseUrl': 'https://bogus.net'})
+
+    def test_rootUrl_set_correctly(self):
+        client = self.clientClass({'rootUrl': self.test_root_url})
+        self.assertEqual(client.options['rootUrl'], self.test_root_url)
+
+    def test_apiVersion_set_correctly(self):
+        client = self.clientClass({'rootUrl': self.test_root_url})
+        self.assertEqual(client.apiVersion, 'v1')
+
+    def test_apiVersion_set_correctly_default(self):
+        apiRef = copy.deepcopy(self.apiRef)
+        del apiRef['reference']['apiVersion']
+        clientClass = subject.createApiClient('testApi', apiRef)
+        client = clientClass({'rootUrl': self.test_root_url})
+        self.assertEqual(client.apiVersion, 'v1')
+
+    def test_serviceName_set_correctly(self):
+        client = self.clientClass({'rootUrl': self.test_root_url})
+        self.assertEqual(client.serviceName, 'fake')
+
+
 class TestSubArgsInRoute(ClientTest):
 
     def test_valid_no_subs(self):
         provided = {'route': '/no/args/here', 'name': 'test'}
         expected = 'no/args/here'
         result = self.client._subArgsInRoute(provided, {})
         self.assertEqual(expected, result)
 
@@ -163,87 +191,87 @@ class TestProcessArgs(ClientTest):
     def test_calling_convention_1_without_payload(self):
         params, payload, query, _, _ = self.client._processArgs({'args': ['k1', 'k2'], 'name': 'test'}, 1, 2)
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, None)
         self.assertEqual(query, {})
 
     def test_calling_convention_1_with_payload(self):
         params, payload, query, _, _ = self.client._processArgs(
-          {'args': ['k1', 'k2'], 'name': 'test', 'input': True},
-          1,
-          2,
-          {'A': 123}
+            {'args': ['k1', 'k2'], 'name': 'test', 'input': True},
+            1,
+            2,
+            {'A': 123}
         )
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, {'A': 123})
         self.assertEqual(query, {})
 
     def test_calling_convention_2_without_payload(self):
         params, payload, query, _, _ = self.client._processArgs({'args': ['k1', 'k2'], 'name': 'test'}, k1=1, k2=2)
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, None)
         self.assertEqual(query, {})
 
     def test_calling_convention_2_with_payload(self):
         params, payload, query, _, _ = self.client._processArgs(
-          {'args': ['k1', 'k2'], 'name': 'test', 'input': True},
-          {'A': 123}, k1=1, k2=2
+            {'args': ['k1', 'k2'], 'name': 'test', 'input': True},
+            {'A': 123}, k1=1, k2=2
         )
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, {'A': 123})
         self.assertEqual(query, {})
 
     def test_calling_convention_3_without_payload_without_query(self):
         params, payload, query, _, _ = self.client._processArgs(
-          {'args': ['k1', 'k2'], 'name': 'test'},
-          params={'k1': 1, 'k2': 2}
+            {'args': ['k1', 'k2'], 'name': 'test'},
+            params={'k1': 1, 'k2': 2}
         )
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, None)
         self.assertEqual(query, {})
 
     def test_calling_convention_3_with_payload_without_query(self):
         params, payload, query, _, _ = self.client._processArgs(
-          {'args': ['k1', 'k2'], 'name': 'test'},
-          params={'k1': 1, 'k2': 2},
-          payload={'A': 123}
+            {'args': ['k1', 'k2'], 'name': 'test'},
+            params={'k1': 1, 'k2': 2},
+            payload={'A': 123}
         )
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, {'A': 123})
         self.assertEqual(query, {})
 
     def test_calling_convention_3_with_payload_with_query(self):
         params, payload, query, _, _ = self.client._processArgs(
-          {'args': ['k1', 'k2'], 'name': 'test'},
-          params={'k1': 1, 'k2': 2},
-          payload={'A': 123},
-          query={'B': 456}
+            {'args': ['k1', 'k2'], 'name': 'test'},
+            params={'k1': 1, 'k2': 2},
+            payload={'A': 123},
+            query={'B': 456}
         )
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, {'A': 123})
         self.assertEqual(query, {'B': 456})
 
     def test_calling_convention_3_without_payload_with_query(self):
         params, payload, query, _, _ = self.client._processArgs(
-          {'args': ['k1', 'k2'], 'name': 'test'},
-          params={'k1': 1, 'k2': 2},
-          query={'B': 456}
+            {'args': ['k1', 'k2'], 'name': 'test'},
+            params={'k1': 1, 'k2': 2},
+            query={'B': 456}
         )
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, None)
         self.assertEqual(query, {'B': 456})
 
     def test_calling_convention_3_with_positional_arguments_with_payload_with_query(self):
         params, payload, query, _, _ = self.client._processArgs(
-          {'args': ['k1', 'k2'], 'name': 'test'},
-          1,
-          2,
-          query={'B': 456},
-          payload={'A': 123}
+            {'args': ['k1', 'k2'], 'name': 'test'},
+            1,
+            2,
+            query={'B': 456},
+            payload={'A': 123}
         )
         self.assertEqual(params, {'k1': 1, 'k2': 2})
         self.assertEqual(payload, {'A': 123})
         self.assertEqual(query, {'B': 456})
 
     def test_calling_convention_3_with_pagination(self):
         def a(x):
             return x
@@ -253,22 +281,22 @@ class TestProcessArgs(ClientTest):
             'name': 'test',
             'query': ['continuationToken', 'limit'],
         }, 1, 2, paginationHandler=a)
         self.assertIs(ph, a)
 
     def test_calling_convention_3_with_pos_args_same_as_param_kwarg_dict_vals_with_payload_with_query(self):
         with self.assertRaises(exc.TaskclusterFailure):
             params, payload, query, _, _ = self.client._processArgs(
-              {'args': ['k1', 'k2'], 'name': 'test'},
-              1,
-              2,
-              params={'k1': 1, 'k2': 2},
-              query={'B': 456},
-              payload={'A': 123}
+                {'args': ['k1', 'k2'], 'name': 'test'},
+                1,
+                2,
+                params={'k1': 1, 'k2': 2},
+                query={'B': 456},
+                payload={'A': 123}
             )
 
 
 # This could probably be done better with Mock
 class ObjWithDotJson(object):
 
     def __init__(self, status_code, x):
         self.status_code = status_code
@@ -279,54 +307,56 @@ class ObjWithDotJson(object):
 
     def raise_for_status(self):
         if self.status_code >= 300 or self.status_code < 200:
             raise requests.exceptions.HTTPError()
 
 
 class TestMakeHttpRequest(ClientTest):
 
+    apiPath = liburls.api(ClientTest.test_root_url, 'fake', 'v1', 'test')
+
     def setUp(self):
 
         ClientTest.setUp(self)
 
     def test_success_first_try(self):
         with mock.patch.object(utils, 'makeSingleHttpRequest') as p:
             expected = {'test': 'works'}
             p.return_value = ObjWithDotJson(200, expected)
 
-            v = self.client._makeHttpRequest('GET', 'http://www.example.com', None)
-            p.assert_called_once_with('GET', 'http://www.example.com', None, mock.ANY)
+            v = self.client._makeHttpRequest('GET', 'test', None)
+            p.assert_called_once_with('GET', self.apiPath, None, mock.ANY)
             self.assertEqual(expected, v)
 
     def test_success_first_try_payload(self):
         with mock.patch.object(utils, 'makeSingleHttpRequest') as p:
             expected = {'test': 'works'}
             p.return_value = ObjWithDotJson(200, expected)
 
-            v = self.client._makeHttpRequest('GET', 'http://www.example.com', {'payload': 2})
-            p.assert_called_once_with('GET', 'http://www.example.com',
+            v = self.client._makeHttpRequest('GET', 'test', {'payload': 2})
+            p.assert_called_once_with('GET', self.apiPath,
                                       utils.dumpJson({'payload': 2}), mock.ANY)
             self.assertEqual(expected, v)
 
     def test_success_fifth_try_status_code(self):
         with mock.patch.object(utils, 'makeSingleHttpRequest') as p:
             expected = {'test': 'works'}
             sideEffect = [
                 ObjWithDotJson(500, None),
                 ObjWithDotJson(500, None),
                 ObjWithDotJson(500, None),
                 ObjWithDotJson(500, None),
                 ObjWithDotJson(200, expected)
             ]
             p.side_effect = sideEffect
-            expectedCalls = [mock.call('GET', 'http://www.example.com', None, mock.ANY)
+            expectedCalls = [mock.call('GET', self.apiPath, None, mock.ANY)
                              for x in range(self.client.options['maxRetries'])]
 
-            v = self.client._makeHttpRequest('GET', 'http://www.example.com', None)
+            v = self.client._makeHttpRequest('GET', 'test', None)
             p.assert_has_calls(expectedCalls)
             self.assertEqual(expected, v)
 
     def test_exhaust_retries_try_status_code(self):
         with mock.patch.object(utils, 'makeSingleHttpRequest') as p:
             msg = {'message': 'msg', 'test': 'works'}
             sideEffect = [
                 ObjWithDotJson(500, msg),
@@ -338,22 +368,22 @@ class TestMakeHttpRequest(ClientTest):
                 ObjWithDotJson(500, msg),
                 ObjWithDotJson(500, msg),
                 ObjWithDotJson(500, msg),
                 ObjWithDotJson(500, msg),
                 ObjWithDotJson(500, msg),
                 ObjWithDotJson(200, {'got this': 'wrong'})
             ]
             p.side_effect = sideEffect
-            expectedCalls = [mock.call('GET', 'http://www.example.com', None, mock.ANY)
+            expectedCalls = [mock.call('GET', self.apiPath, None, mock.ANY)
                              for x in range(self.client.options['maxRetries'] + 1)]
 
             with self.assertRaises(exc.TaskclusterRestFailure):
                 try:
-                    self.client._makeHttpRequest('GET', 'http://www.example.com', None)
+                    self.client._makeHttpRequest('GET', 'test', None)
                 except exc.TaskclusterRestFailure as err:
                     self.assertEqual('msg', str(err))
                     self.assertEqual(500, err.status_code)
                     self.assertEqual(msg, err.body)
                     raise err
             p.assert_has_calls(expectedCalls)
 
     def test_success_fifth_try_connection_errors(self):
@@ -362,65 +392,59 @@ class TestMakeHttpRequest(ClientTest):
             sideEffect = [
                 requests.exceptions.RequestException,
                 requests.exceptions.RequestException,
                 requests.exceptions.RequestException,
                 requests.exceptions.RequestException,
                 ObjWithDotJson(200, expected)
             ]
             p.side_effect = sideEffect
-            expectedCalls = [mock.call('GET', 'http://www.example.com', None, mock.ANY)
+            expectedCalls = [mock.call('GET', self.apiPath, None, mock.ANY)
                              for x in range(self.client.options['maxRetries'])]
 
-            v = self.client._makeHttpRequest('GET', 'http://www.example.com', None)
+            v = self.client._makeHttpRequest('GET', 'test', None)
             p.assert_has_calls(expectedCalls)
             self.assertEqual(expected, v)
 
     def test_failure_status_code(self):
         with mock.patch.object(utils, 'makeSingleHttpRequest') as p:
             p.return_value = ObjWithDotJson(500, None)
-            expectedCalls = [mock.call('GET', 'http://www.example.com', None, mock.ANY)
+            expectedCalls = [mock.call('GET', self.apiPath, None, mock.ANY)
                              for x in range(self.client.options['maxRetries'])]
             with self.assertRaises(exc.TaskclusterRestFailure):
-                self.client._makeHttpRequest('GET', 'http://www.example.com', None)
+                self.client._makeHttpRequest('GET', 'test', None)
             p.assert_has_calls(expectedCalls)
 
     def test_failure_connection_errors(self):
         with mock.patch.object(utils, 'makeSingleHttpRequest') as p:
             p.side_effect = requests.exceptions.RequestException
-            expectedCalls = [mock.call('GET', 'http://www.example.com', None, mock.ANY)
+            expectedCalls = [mock.call('GET', self.apiPath, None, mock.ANY)
                              for x in range(self.client.options['maxRetries'])]
             with self.assertRaises(exc.TaskclusterConnectionError):
-                self.client._makeHttpRequest('GET', 'http://www.example.com', None)
+                self.client._makeHttpRequest('GET', 'test', None)
             p.assert_has_calls(expectedCalls)
 
 
 class TestOptions(ClientTest):
 
-    def setUp(self):
-        ClientTest.setUp(self)
-        self.clientClass2 = subject.createApiClient('testApi', base.createApiRef())
-        self.client2 = self.clientClass2({'baseUrl': 'http://notlocalhost:5888/v2'})
-
-    def test_defaults_should_work(self):
-        self.assertEqual(self.client.options['baseUrl'], 'https://fake.taskcluster.net/v1')
-        self.assertEqual(self.client2.options['baseUrl'], 'http://notlocalhost:5888/v2')
-
     def test_change_default_doesnt_change_previous_instances(self):
         prevMaxRetries = subject._defaultConfig['maxRetries']
         with mock.patch.dict(subject._defaultConfig, {'maxRetries': prevMaxRetries + 1}):
             self.assertEqual(self.client.options['maxRetries'], prevMaxRetries)
 
     def test_credentials_which_cannot_be_encoded_in_unicode_work(self):
         badCredentials = {
             'accessToken': u"\U0001F4A9",
             'clientId': u"\U0001F4A9",
         }
         with self.assertRaises(exc.TaskclusterAuthFailure):
-            subject.Auth({'credentials': badCredentials})
+            subject.Auth({
+                'rootUrl': self.real_root_url,
+                'credentials': badCredentials,
+            })
 
 
 class TestMakeApiCall(ClientTest):
     """ This class covers both the _makeApiCall function logic as well as the
     logic involved in setting up the api member functions since these are very
     related things"""
 
     def setUp(self):
@@ -513,23 +537,23 @@ class TestMakeApiCall(ClientTest):
 class TestTopicExchange(ClientTest):
 
     def test_string_pass_through(self):
         expected = 'johnwrotethis'
         actual = self.client.topicName(expected)
         self.assertEqual(expected, actual['routingKeyPattern'])
 
     def test_exchange(self):
-        expected = 'test/v1/topicExchange'
+        expected = 'exchange/taskcluster-fake/v1/topicExchange'
         actual = self.client.topicName('')
         self.assertEqual(expected, actual['exchange'])
 
     def test_exchange_trailing_slash(self):
-        self.client.options['exchangePrefix'] = 'test/v1/'
-        expected = 'test/v1/topicExchange'
+        self.client.options['exchangePrefix'] = 'exchange/taskcluster-fake2/v1/'
+        expected = 'exchange/taskcluster-fake2/v1/topicExchange'
         actual = self.client.topicName('')
         self.assertEqual(expected, actual['exchange'])
 
     def test_constant(self):
         expected = 'primary.*.*.*.#'
         actual = self.client.topicName({})
         self.assertEqual(expected, actual['routingKeyPattern'])
 
@@ -551,60 +575,59 @@ class TestTopicExchange(ClientTest):
         actual = self.client.topicName()
         self.assertEqual(expected, actual['routingKeyPattern'])
         actual = self.client.topicName({})
         self.assertEqual(expected, actual['routingKeyPattern'])
 
 
 class TestBuildUrl(ClientTest):
 
+    apiPath = liburls.api(ClientTest.test_root_url, 'fake', 'v1', 'two_args_no_input/arg0/arg1')
+
     def test_build_url_positional(self):
-        expected = 'https://fake.taskcluster.net/v1/two_args_no_input/arg0/arg1'
         actual = self.client.buildUrl('two_args_no_input', 'arg0', 'arg1')
-        self.assertEqual(expected, actual)
+        self.assertEqual(self.apiPath, actual)
 
     def test_build_url_keyword(self):
-        expected = 'https://fake.taskcluster.net/v1/two_args_no_input/arg0/arg1'
         actual = self.client.buildUrl('two_args_no_input', arg0='arg0', arg1='arg1')
-        self.assertEqual(expected, actual)
+        self.assertEqual(self.apiPath, actual)
 
     def test_build_url_query_string(self):
-        expected = 'https://fake.taskcluster.net/v1/two_args_no_input/arg0/arg1?qs0=1'
         actual = self.client.buildUrl(
             'two_args_no_input',
             params={
                 'arg0': 'arg0',
                 'arg1': 'arg1'
             },
             query={'qs0': 1}
         )
-        self.assertEqual(expected, actual)
+        self.assertEqual(self.apiPath + '?qs0=1', actual)
 
     def test_fails_to_build_url_for_missing_method(self):
         with self.assertRaises(exc.TaskclusterFailure):
             self.client.buildUrl('non-existing')
 
     def test_fails_to_build_not_enough_args(self):
         with self.assertRaises(exc.TaskclusterFailure):
             self.client.buildUrl('two_args_no_input', 'not-enough-args')
 
 
 class TestBuildSignedUrl(ClientTest):
 
+    apiPath = liburls.api(ClientTest.test_root_url, 'fake', 'v1', 'two_args_no_input/arg0/arg1')
+
     def test_builds_surl_positional(self):
-        expected = 'https://fake.taskcluster.net/v1/two_args_no_input/arg0/arg1?bewit=X'
         actual = self.client.buildSignedUrl('two_args_no_input', 'arg0', 'arg1')
         actual = re.sub('bewit=[^&]*', 'bewit=X', actual)
-        self.assertEqual(expected, actual)
+        self.assertEqual(self.apiPath + '?bewit=X', actual)
 
     def test_builds_surl_keyword(self):
-        expected = 'https://fake.taskcluster.net/v1/two_args_no_input/arg0/arg1?bewit=X'
         actual = self.client.buildSignedUrl('two_args_no_input', arg0='arg0', arg1='arg1')
         actual = re.sub('bewit=[^&]*', 'bewit=X', actual)
-        self.assertEqual(expected, actual)
+        self.assertEqual(self.apiPath + '?bewit=X', actual)
 
 
 class TestMockHttpCalls(ClientTest):
 
     """Test entire calls down to the requests layer, ensuring they have
     well-formed URLs and handle request and response bodies properly.  This
     verifies that we can call real methods with both position and keyword
     args"""
@@ -617,129 +640,134 @@ class TestMockHttpCalls(ClientTest):
             self.gotUrl = urllib.parse.urlunsplit(url)
             self.gotRequest = request
             return self.fakeResponse
         self.fakeSite = fakeSite
 
     def test_no_args_no_input(self):
         with httmock.HTTMock(self.fakeSite):
             self.client.no_args_no_input()
-        self.assertEqual(self.gotUrl, 'https://fake.taskcluster.net/v1/no_args_no_input')
+        self.assertEqual(self.gotUrl, 'https://tc-tests.example.com/api/fake/v1/no_args_no_input')
 
     def test_two_args_no_input(self):
         with httmock.HTTMock(self.fakeSite):
             self.client.two_args_no_input('1', '2')
-        self.assertEqual(self.gotUrl, 'https://fake.taskcluster.net/v1/two_args_no_input/1/2')
+        self.assertEqual(self.gotUrl, 'https://tc-tests.example.com/api/fake/v1/two_args_no_input/1/2')
 
     def test_no_args_with_input(self):
         with httmock.HTTMock(self.fakeSite):
             self.client.no_args_with_input({'x': 1})
-        self.assertEqual(self.gotUrl, 'https://fake.taskcluster.net/v1/no_args_with_input')
+        self.assertEqual(self.gotUrl, 'https://tc-tests.example.com/api/fake/v1/no_args_with_input')
         self.assertEqual(json.loads(self.gotRequest.body), {"x": 1})
 
     def test_no_args_with_empty_input(self):
         with httmock.HTTMock(self.fakeSite):
             self.client.no_args_with_input({})
-        self.assertEqual(self.gotUrl, 'https://fake.taskcluster.net/v1/no_args_with_input')
+        self.assertEqual(self.gotUrl, 'https://tc-tests.example.com/api/fake/v1/no_args_with_input')
         self.assertEqual(json.loads(self.gotRequest.body), {})
 
     def test_two_args_with_input(self):
         with httmock.HTTMock(self.fakeSite):
             self.client.two_args_with_input('a', 'b', {'x': 1})
         self.assertEqual(self.gotUrl,
-                         'https://fake.taskcluster.net/v1/two_args_with_input/a/b')
+                         'https://tc-tests.example.com/api/fake/v1/two_args_with_input/a/b')
         self.assertEqual(json.loads(self.gotRequest.body), {"x": 1})
 
     def test_kwargs(self):
         with httmock.HTTMock(self.fakeSite):
             self.client.two_args_with_input(
                 {'x': 1}, arg0='a', arg1='b')
         self.assertEqual(self.gotUrl,
-                         'https://fake.taskcluster.net/v1/two_args_with_input/a/b')
+                         'https://tc-tests.example.com/api/fake/v1/two_args_with_input/a/b')
         self.assertEqual(json.loads(self.gotRequest.body), {"x": 1})
 
 
 @unittest.skipIf(os.environ.get('NO_TESTS_OVER_WIRE'), "Skipping tests over wire")
 class TestAuthentication(base.TCTest):
 
     def test_no_creds_needed(self):
         """we can call methods which require no scopes with an unauthenticated
         client"""
         # mock this request so we don't depend on the existence of a client
         @httmock.all_requests
         def auth_response(url, request):
             self.assertEqual(urllib.parse.urlunsplit(url),
-                             'https://auth.taskcluster.net/v1/clients/abc')
+                             'https://tc-tests.example.com/api/auth/v1/clients/abc')
             self.failIf('Authorization' in request.headers)
             headers = {'content-type': 'application/json'}
             content = {"clientId": "abc"}
             return httmock.response(200, content, headers, None, 5, request)
 
         with httmock.HTTMock(auth_response):
-            client = subject.Auth({"credentials": {}})
+            client = subject.Auth({"rootUrl": "https://tc-tests.example.com", "credentials": {}})
             result = client.client('abc')
             self.assertEqual(result, {"clientId": "abc"})
 
     def test_permacred_simple(self):
         """we can call methods which require authentication with valid
         permacreds"""
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': {
                 'clientId': 'tester',
                 'accessToken': 'no-secret',
             }
         })
         result = client.testAuthenticate({
             'clientScopes': ['test:a'],
             'requiredScopes': ['test:a'],
         })
         self.assertEqual(result, {'scopes': ['test:a'], 'clientId': 'tester'})
 
     def test_permacred_simple_authorizedScopes(self):
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': {
                 'clientId': 'tester',
                 'accessToken': 'no-secret',
             },
             'authorizedScopes': ['test:a', 'test:b'],
         })
         result = client.testAuthenticate({
             'clientScopes': ['test:*'],
             'requiredScopes': ['test:a'],
         })
         self.assertEqual(result, {'scopes': ['test:a', 'test:b'],
                                   'clientId': 'tester'})
 
     def test_unicode_permacred_simple(self):
         """Unicode strings that encode to ASCII in credentials do not cause issues"""
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': {
                 'clientId': u'tester',
                 'accessToken': u'no-secret',
             }
         })
         result = client.testAuthenticate({
             'clientScopes': ['test:a'],
             'requiredScopes': ['test:a'],
         })
         self.assertEqual(result, {'scopes': ['test:a'], 'clientId': 'tester'})
 
     def test_invalid_unicode_permacred_simple(self):
         """Unicode strings that do not encode to ASCII in credentials cause issues"""
         with self.assertRaises(exc.TaskclusterAuthFailure):
             subject.Auth({
+                'rootUrl': self.test_root_url,
                 'credentials': {
                     'clientId': u"\U0001F4A9",
                     'accessToken': u"\U0001F4A9",
                 }
             })
 
     def test_permacred_insufficient_scopes(self):
         """A call with insufficient scopes results in an error"""
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': {
                 'clientId': 'tester',
                 'accessToken': 'no-secret',
             }
         })
         # TODO: this should be TaskclsuterAuthFailure; most likely the client
         # is expecting AuthorizationFailure instead of AuthenticationFailure
         with self.assertRaises(exc.TaskclusterRestFailure):
@@ -754,16 +782,17 @@ class TestAuthentication(base.TCTest):
         tempCred = subject.createTemporaryCredentials(
             'tester',
             'no-secret',
             datetime.datetime.utcnow(),
             datetime.datetime.utcnow() + datetime.timedelta(hours=1),
             ['test:xyz'],
         )
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': tempCred,
         })
 
         result = client.testAuthenticate({
             'clientScopes': ['test:*'],
             'requiredScopes': ['test:xyz'],
         })
         self.assertEqual(result, {'scopes': ['test:xyz'], 'clientId': 'tester'})
@@ -773,16 +802,17 @@ class TestAuthentication(base.TCTest):
             'tester',
             'no-secret',
             datetime.datetime.utcnow(),
             datetime.datetime.utcnow() + datetime.timedelta(hours=1),
             ['test:xyz'],
             name='credName'
         )
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': tempCred,
         })
 
         result = client.testAuthenticate({
             'clientScopes': ['test:*', 'auth:create-client:credName'],
             'requiredScopes': ['test:xyz'],
         })
         self.assertEqual(result, {'scopes': ['test:xyz'], 'clientId': 'credName'})
@@ -791,16 +821,17 @@ class TestAuthentication(base.TCTest):
         tempCred = subject.createTemporaryCredentials(
             'tester',
             'no-secret',
             datetime.datetime.utcnow(),
             datetime.datetime.utcnow() + datetime.timedelta(hours=1),
             ['test:xyz:*'],
         )
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': tempCred,
             'authorizedScopes': ['test:xyz:abc'],
         })
 
         result = client.testAuthenticate({
             'clientScopes': ['test:*'],
             'requiredScopes': ['test:xyz:abc'],
         })
@@ -812,30 +843,32 @@ class TestAuthentication(base.TCTest):
             'tester',
             'no-secret',
             datetime.datetime.utcnow(),
             datetime.datetime.utcnow() + datetime.timedelta(hours=1),
             ['test:xyz:*'],
             name='credName'
         )
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': tempCred,
             'authorizedScopes': ['test:xyz:abc'],
         })
 
         result = client.testAuthenticate({
             'clientScopes': ['test:*', 'auth:create-client:credName'],
             'requiredScopes': ['test:xyz:abc'],
         })
         self.assertEqual(result, {'scopes': ['test:xyz:abc'],
                                   'clientId': 'credName'})
 
     def test_signed_url(self):
         """we can use a signed url built with the python client"""
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': {
                 'clientId': 'tester',
                 'accessToken': 'no-secret',
             }
         })
         signedUrl = client.buildSignedUrl('testAuthenticateGet')
         response = requests.get(signedUrl)
         response.raise_for_status()
@@ -843,16 +876,17 @@ class TestAuthentication(base.TCTest):
         response['scopes'].sort()
         self.assertEqual(response, {
             'scopes': sorted(['test:*', u'auth:create-client:test:*']),
             'clientId': 'tester',
         })
 
     def test_signed_url_bad_credentials(self):
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': {
                 'clientId': 'tester',
                 'accessToken': 'wrong-secret',
             }
         })
         signedUrl = client.buildSignedUrl('testAuthenticateGet')
         response = requests.get(signedUrl)
         with self.assertRaises(requests.exceptions.RequestException):
@@ -863,29 +897,31 @@ class TestAuthentication(base.TCTest):
         tempCred = subject.createTemporaryCredentials(
             'tester',
             'no-secret',
             datetime.datetime.utcnow(),
             datetime.datetime.utcnow() + datetime.timedelta(hours=1),
             ['test:*'],
         )
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': tempCred,
         })
         signedUrl = client.buildSignedUrl('testAuthenticateGet')
         response = requests.get(signedUrl)
         response.raise_for_status()
         response = response.json()
         self.assertEqual(response, {
             'scopes': ['test:*'],
             'clientId': 'tester',
         })
 
     def test_signed_url_authorizedScopes(self):
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': {
                 'clientId': 'tester',
                 'accessToken': 'no-secret',
             },
             'authorizedScopes': ['test:authenticate-get'],
         })
         signedUrl = client.buildSignedUrl('testAuthenticateGet')
         response = requests.get(signedUrl)
@@ -900,16 +936,17 @@ class TestAuthentication(base.TCTest):
         tempCred = subject.createTemporaryCredentials(
             'tester',
             'no-secret',
             datetime.datetime.utcnow(),
             datetime.datetime.utcnow() + datetime.timedelta(hours=1),
             ['test:*'],
         )
         client = subject.Auth({
+            'rootUrl': self.real_root_url,
             'credentials': tempCred,
             'authorizedScopes': ['test:authenticate-get'],
         })
         signedUrl = client.buildSignedUrl('testAuthenticateGet')
         response = requests.get(signedUrl)
         response.raise_for_status()
         response = response.json()
         self.assertEqual(response, {
--- a/third_party/python/taskcluster/test/test_utils.py
+++ b/third_party/python/taskcluster/test/test_utils.py
@@ -1,10 +1,11 @@
 import datetime
 import uuid
+import os
 
 import taskcluster.utils as subject
 import dateutil.parser
 import httmock
 import mock
 import requests
 
 import base
@@ -333,8 +334,106 @@ class TestIsExpired(TestCase):
             "scopes":["*"],
             "start":1450740520182,
             "expiry":0,
             "seed":"90PyTwYxS96-lBPc0f_MqQGV-hHCUsTYWpXZilv6EqDg",
             "signature":"HocA2IiCoGzjUQZbrbLSwKMXZSYWCu/hfMPCa/ovggQ="
           }
         """)
         self.assertEqual(isExpired, True)
+
+
+class TestFromEnv(TestCase):
+
+    def clear_env(self):
+        for v in 'ROOT_URL', 'CLIENT_ID', 'ACCESS_TOKEN', 'CERTIFICATE':
+            v = 'TASKCLUSTER_' + v
+            if v in os.environ:
+                del os.environ[v]
+
+    @mock.patch.dict(os.environ)
+    def test_empty(self):
+        self.clear_env()
+        self.assertEqual(subject.optionsFromEnvironment(), {})
+
+    @mock.patch.dict(os.environ)
+    def test_all(self):
+        os.environ['TASKCLUSTER_ROOT_URL'] = 'https://tc.example.com'
+        os.environ['TASKCLUSTER_CLIENT_ID'] = 'me'
+        os.environ['TASKCLUSTER_ACCESS_TOKEN'] = 'shave-and-a-haircut'
+        os.environ['TASKCLUSTER_CERTIFICATE'] = '{"bits":2}'
+        self.assertEqual(subject.optionsFromEnvironment(), {
+            'rootUrl': 'https://tc.example.com',
+            'credentials': {
+                'clientId': 'me',
+                'accessToken': 'shave-and-a-haircut',
+                'certificate': '{"bits":2}',
+            },
+        })
+
+    @mock.patch.dict(os.environ)
+    def test_cred_only(self):
+        os.environ['TASKCLUSTER_ACCESS_TOKEN'] = 'shave-and-a-haircut'
+        self.assertEqual(subject.optionsFromEnvironment(), {
+            'credentials': {
+                'accessToken': 'shave-and-a-haircut',
+            },
+        })
+
+    @mock.patch.dict(os.environ)
+    def test_rooturl_only(self):
+        os.environ['TASKCLUSTER_ROOT_URL'] = 'https://tc.example.com'
+        self.assertEqual(subject.optionsFromEnvironment(), {
+            'rootUrl': 'https://tc.example.com',
+        })
+
+    @mock.patch.dict(os.environ)
+    def test_default_rooturl(self):
+        os.environ['TASKCLUSTER_CLIENT_ID'] = 'me'
+        os.environ['TASKCLUSTER_ACCESS_TOKEN'] = 'shave-and-a-haircut'
+        os.environ['TASKCLUSTER_CERTIFICATE'] = '{"bits":2}'
+        self.assertEqual(
+            subject.optionsFromEnvironment({'rootUrl': 'https://other.example.com'}), {
+                'rootUrl': 'https://other.example.com',
+                'credentials': {
+                    'clientId': 'me',
+                    'accessToken': 'shave-and-a-haircut',
+                    'certificate': '{"bits":2}',
+                    },
+                })
+
+    @mock.patch.dict(os.environ)
+    def test_default_rooturl_overridden(self):
+        os.environ['TASKCLUSTER_ROOT_URL'] = 'https://tc.example.com'
+        self.assertEqual(
+            subject.optionsFromEnvironment({'rootUrl': 'https://other.example.com'}),
+            {'rootUrl': 'https://tc.example.com'})
+
+    @mock.patch.dict(os.environ)
+    def test_default_creds(self):
+        os.environ['TASKCLUSTER_ROOT_URL'] = 'https://tc.example.com'
+        os.environ['TASKCLUSTER_ACCESS_TOKEN'] = 'shave-and-a-haircut'
+        os.environ['TASKCLUSTER_CERTIFICATE'] = '{"bits":2}'
+        self.assertEqual(
+            subject.optionsFromEnvironment({'credentials': {'clientId': 'them'}}), {
+                'rootUrl': 'https://tc.example.com',
+                'credentials': {
+                    'clientId': 'them',
+                    'accessToken': 'shave-and-a-haircut',
+                    'certificate': '{"bits":2}',
+                    },
+                })
+
+    @mock.patch.dict(os.environ)
+    def test_default_creds_overridden(self):
+        os.environ['TASKCLUSTER_ROOT_URL'] = 'https://tc.example.com'
+        os.environ['TASKCLUSTER_CLIENT_ID'] = 'me'
+        os.environ['TASKCLUSTER_ACCESS_TOKEN'] = 'shave-and-a-haircut'
+        os.environ['TASKCLUSTER_CERTIFICATE'] = '{"bits":2}'
+        self.assertEqual(
+            subject.optionsFromEnvironment({'credentials': {'clientId': 'them'}}), {
+                'rootUrl': 'https://tc.example.com',
+                'credentials': {
+                    'clientId': 'me',
+                    'accessToken': 'shave-and-a-haircut',
+                    'certificate': '{"bits":2}',
+                    },
+                })