Package: taskcluster
Taskcluster Client for PythonA Taskcluster client library for Python.This library is a complete interface to Taskcluster in Python.  It provides
both synchronous and asynchronous interfaces for all Taskcluster API methods.UsageFor a general guide to using Taskcluster clients, seeCalling Taskcluster APIs.SetupBefore calling an API end-point, you'll need to create a client instance.
There is a class for each service, e.g.,QueueandAuth.  Each takes the
same options, described below.  Note that onlyrootUrlis
required, and it's unusual to configure any other options aside fromcredentials.For each service, there are sync and async variants.  The classes undertaskcluster(e.g.,taskcluster.Queue) operate synchronously.  The classes
undertaskcluster.aio(e.g.,taskcluster.aio.Queue) are asynchronous.Authentication OptionsHere is a simple set-up of an Index client:importtaskclusterindex=taskcluster.Index({'rootUrl':'https://tc.example.com','credentials':{'clientId':'id','accessToken':'accessToken'},})TherootUrloption is required as it gives the Taskcluster deployment to
which API requests should be sent.  Credentials are only required if the
request is to be authenticated -- many Taskcluster API methods do not require
authentication.In most cases, the root URL and Taskcluster credentials should be provided instandard environment variables.  Usetaskcluster.optionsFromEnvironment()to read these variables automatically:auth=taskcluster.Auth(taskcluster.optionsFromEnvironment())Note that this function does not respectTASKCLUSTER_PROXY_URL.  To use the Taskcluster Proxy from within a task:auth=taskcluster.Auth({'rootUrl':os.environ['TASKCLUSTER_PROXY_URL']})Authorized ScopesIf you wish to perform requests on behalf of a third-party that has small set
of scopes than you do. You can specifywhich scopes your request should be
allowed to
use,
in theauthorizedScopesoption.opts=taskcluster.optionsFromEnvironment()opts['authorizedScopes']=['queue:create-task:highest:my-provisioner/my-worker-type']queue=taskcluster.Queue(opts)Other OptionsThe following additional options are accepted when constructing a client object:signedUrlExpiration- default value for theexpirationargument tobuildSignedUrlmaxRetries- maximum number of times to retry a failed requestCalling API MethodsAPI methods are available as methods on the corresponding client object.  For
sync clients, these are sync methods, and for async clients they are async
methods; the calling convention is the same in either case.There are four calling conventions for methods:client.method(v1,v1,payload)client.method(payload,k1=v1,k2=v2)client.method(payload=payload,query=query,params={k1:v1,k2:v2})client.method(v1,v2,payload=payload,query=query)Here,v1andv2are URL parameters (namedk1andk2),payloadis the
request payload, andqueryis a dictionary of query arguments.For example, in order to call an API method with query-string arguments:awaitqueue.listTaskGroup('JzTGxwxhQ76_Tt1dxkaG5g',query={'continuationToken':previousResponse.get('continuationToken')})Generating URLsIt is often necessary to generate the URL for an API method without actually calling the method.
To do so, usebuildUrlor, for an API method that requires authentication,buildSignedUrl.importtaskclusterindex=taskcluster.Index(taskcluster.optionsFromEnvironment())print(index.buildUrl('findTask','builds.v1.latest'))secrets=taskcluster.Secrets(taskcluster.optionsFromEnvironment())print(secret.buildSignedUrl('get','my-secret'))Note that signed URLs are time-limited; the expiration can be set with thesignedUrlExpirationoption to the client constructor, or with theexpirationkeyword arguement tobuildSignedUrl, both given in seconds.Generating Temporary CredentialsIf you have non-temporary taskcluster credentials you can generate a set oftemporary credentialsas follows. Notice that the credentials cannot last more
than 31 days, and you can only revoke them by revoking the credentials that was
used to issue them (this takes up to one hour).It is not the responsibility of the caller to apply any clock drift adjustment
to the start or expiry time - this is handled by the auth service directly.importdatetimestart=datetime.datetime.now()expiry=start+datetime.timedelta(0,60)scopes=['ScopeA','ScopeB']name='foo'credentials=taskcluster.createTemporaryCredentials(# issuing clientIdclientId,# issuing accessTokenaccessToken,# Validity of temporary credentials starts here, in timestampstart,# Expiration of temporary credentials, in timestampexpiry,# Scopes to grant the temporary credentialsscopes,# credential name (optional)name)You cannot use temporary credentials to issue new temporary credentials.  You
must haveauth:create-client:<name>to create a named temporary credential,
but unnamed temporary credentials can be created regardless of your scopes.Handling TimestampsMany taskcluster APIs require ISO 8601 time stamps offset into the future
as way of providing expiration, deadlines, etc. These can be easily created
usingdatetime.datetime.isoformat(), however, it can be rather error prone
and tedious to offsetdatetime.datetimeobjects into the future. Therefore
this library comes with two utility functions for this purposes.dateObject=taskcluster.fromNow("2 days 3 hours 1 minute")# -> datetime.datetime(2017, 1, 21, 17, 8, 1, 607929)dateString=taskcluster.fromNowJSON("2 days 3 hours 1 minute")# -> '2017-01-21T17:09:23.240178Z'By default it will offset the date time into the future, if the offset strings
are prefixed minus (-) the date object will be offset into the past. This is
useful in some corner cases.dateObject=taskcluster.fromNow("- 1 year 2 months 3 weeks 5 seconds");# -> datetime.datetime(2015, 10, 30, 18, 16, 50, 931161)The offset string is ignorant of whitespace and case insensitive. It may also
optionally be prefixed plus+(if not prefixed minus), any+prefix will be
ignored. However, entries in the offset string must be given in order from
high to low, ie.2 years 1 day. Additionally, various shorthands may be
employed, as illustrated below.years,    year,   yr,   y
  months,   month,  mo
  weeks,    week,         w
  days,     day,          d
  hours,    hour,         h
  minutes,  minute, min
  seconds,  second, sec,  sThefromNowmethod may also be given a date to be relative to as a second
argument. This is useful if offset the task expiration relative to the the task
deadline or doing something similar.  This argument can also be passed as the
kwargdateObjdateObject1=taskcluster.fromNow("2 days 3 hours");dateObject2=taskcluster.fromNow("1 year",dateObject1);taskcluster.fromNow("1 year",dateObj=dateObject1);# -> datetime.datetime(2018, 1, 21, 17, 59, 0, 328934)Generating SlugIDsTo generate slugIds (Taskcluster's client-generated unique IDs), usetaskcluster.slugId(), which will return a unique slugId on each call.In some cases it is useful to be able to create a mapping from names to
slugIds, with the ability to generate the same slugId multiple times.
Thetaskcluster.stableSlugId()function returns a callable that does
just this.gen=taskcluster.stableSlugId()sometask=gen('sometask')assertgen('sometask')==sometask# same input generates same outputassertgen('sometask')!=gen('othertask')gen2=taskcluster.stableSlugId()sometask2=gen('sometask')assertsometask2!=sometask# but different slugId generators produce# different outputScope AnalysisThescopeMatch(assumedScopes, requiredScopeSets)function determines
whether one or more of a set of required scopes are satisfied by the assumed
scopes, taking *-expansion into account.  This is useful for making local
decisions on scope satisfaction, but note thatassumed_scopesmust be theexpandedscopes, as this function cannot perform expansion.It takes a list of a assumed scopes, and a list of required scope sets on
disjunctive normal form, and checks if any of the required scope sets are
satisfied.Example:requiredScopeSets=[["scopeA","scopeB"],["scopeC:*"]]assertscopesMatch(['scopeA','scopeB'],requiredScopeSets)assertscopesMatch(['scopeC:xyz'],requiredScopeSets)assertnotscopesMatch(['scopeA'],requiredScopeSets)assertnotscopesMatch(['scopeC'],requiredScopeSets)PaginationMany Taskcluster API methods are paginated.  There are two ways to handle
pagination easily with the python client.  The first is to implement pagination
in your code:importtaskclusterqueue=taskcluster.Queue({'rootUrl':'https://tc.example.com'})i=0tasks=0outcome=queue.listTaskGroup('JzTGxwxhQ76_Tt1dxkaG5g')whileoutcome.get('continuationToken'):print('Response%dgave us%dmore tasks'%(i,len(outcome['tasks'])))ifoutcome.get('continuationToken'):outcome=queue.listTaskGroup('JzTGxwxhQ76_Tt1dxkaG5g',query={'continuationToken':outcome.get('continuationToken')})i+=1tasks+=len(outcome.get('tasks',[]))print('Task Group%shas%dtasks'%(outcome['taskGroupId'],tasks))There's also an experimental feature to support built in automatic pagination
in the sync client.  This feature allows passing a callback as the
'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:importtaskclusterqueue=taskcluster.Queue({'rootUrl':'https://tc.example.com'})responses=[]defhandle_page(y):print("%dtasks fetched"%len(y.get('tasks',[])))responses.append(y)queue.listTaskGroup('JzTGxwxhQ76_Tt1dxkaG5g',paginationHandler=handle_page)tasks=0forresponseinresponses:tasks+=len(response.get('tasks',[]))print("%drequests fetch%dtasks"%(len(responses),tasks))Pulse EventsThis library can generate exchange patterns for Pulse messages based on the
Exchanges definitions provded by each service.  This is done by instantiating a<service>Eventsclass and calling a method with the name of the vent.
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.fromtaskclusterimportclientqEvt=client.QueueEvents({rootUrl:'https://tc.example.com'})# The following calls are equivalentprint(qEvt.taskCompleted({'taskId':'atask'}))print(qEvt.taskCompleted(taskId='atask'))Note that the client library doesnotprovide support for interfacing with a Pulse server.LoggingLogging is set up intaskcluster/__init__.py.  If the specialDEBUG_TASKCLUSTER_CLIENTenvironment variable is set, the__init__.pymodule will set theloggingmodule's level for its logger tologging.DEBUGand if there are no existing handlers, add alogging.StreamHandler()instance.  This is meant to assist those who do not wish to bother figuring out
how to configure the python logging module but do want debug messagesUploading and Downloading ObjectsThe Object service provides an API for reliable uploads and downloads of large objects.
This library provides convenience methods to implement the client portion of those APIs, providing well-tested, resilient upload and download functionality.
These methods will negotiate the appropriate method with the object service and perform the required steps to transfer the data.All methods are available in both sync and async versions, with identical APIs except for theasync/awaitkeywords.In either case, you will need to provide a configuredObjectinstance with appropriate credentials for the operation.NOTE: There is an helper function to uploads3artifacts,taskcluster.helper.upload_artifact, but it is deprecated as it only supports thes3artifact type.UploadsTo upload, use any of the following:await taskcluster.aio.upload.uploadFromBuf(projectId=.., name=.., contentType=.., contentLength=.., uploadId=.., expires=.., maxRetries=.., objectService=.., data=..)- asynchronously upload data from a buffer full of bytes.await taskcluster.aio.upload.uploadFromFile(projectId=.., name=.., contentType=.., contentLength=.., uploadId=.., expires=.., maxRetries=.., objectService=.., file=..)- asynchronously upload data from a standard Python file.
Note that this isprobably what you want, even in an async context.await taskcluster.aio.upload(projectId=.., name=.., contentType=.., contentLength=.., expires=.., uploadId=.., maxRetries=.., objectService=.., readerFactory=..)- asynchronously upload data from an async reader factory.taskcluster.upload.uploadFromBuf(projectId=.., name=.., contentType=.., contentLength=.., expires=.., uploadId=.., maxRetries=.., objectService=.., data=..)- upload data from a buffer full of bytes.taskcluster.upload.uploadFromFile(projectId=.., name=.., contentType=.., contentLength=.., expires=.., uploadId=.., maxRetries=.., objectService=.., file=..)- upload data from a standard Python file.taskcluster.upload(projectId=.., name=.., contentType=.., contentLength=.., expires=.., uploadId=.., maxRetries=.., objectService=.., readerFactory=..)- upload data from a sync reader factory.A "reader" is an object with aread(max_size=-1)method which reads and returns a chunk of 1 ..max_sizebytes, or returns an empty string at EOF, async for the async functions and sync for the remainder.
A "reader factory" is an async callable which returns a fresh reader, ready to read the first byte of the object.
When uploads are retried, the reader factory may be called more than once.TheuploadIdparameter may be omitted, in which case a new slugId will be generated.DownloadsTo download, use any of the following:await taskcluster.aio.download.downloadToBuf(name=.., maxRetries=.., objectService=..)- asynchronously download an object to an in-memory buffer, returning a tuple (buffer, content-type).
If the file is larger than available memory, this will crash.await taskcluster.aio.download.downloadToFile(name=.., maxRetries=.., objectService=.., file=..)- asynchronously download an object to a standard Python file, returning the content type.await taskcluster.aio.download.download(name=.., maxRetries=.., objectService=.., writerFactory=..)- asynchronously download an object to an async writer factory, returning the content type.taskcluster.download.downloadToBuf(name=.., maxRetries=.., objectService=..)- download an object to an in-memory buffer, returning a tuple (buffer, content-type).
If the file is larger than available memory, this will crash.taskcluster.download.downloadToFile(name=.., maxRetries=.., objectService=.., file=..)- download an object to a standard Python file, returning the content type.taskcluster.download.download(name=.., maxRetries=.., objectService=.., writerFactory=..)- download an object to a sync writer factory, returning the content type.A "writer" is an object with awrite(data)method which writes the given data, async for the async functions and sync for the remainder.
A "writer factory" is a callable (again either async or sync) which returns a fresh writer, ready to write the first byte of the object.
When uploads are retried, the writer factory may be called more than once.Artifact DownloadsArtifacts can be downloaded from the queue service with similar functions to those above.
These functions support all of the queue's storage types, raising an error forerrorartifacts.
In each case, ifrunIdis omitted then the most recent run will be used.await taskcluster.aio.download.downloadArtifactToBuf(taskId=.., runId=.., name=.., maxRetries=.., queueService=..)- asynchronously download an object to an in-memory buffer, returning a tuple (buffer, content-type).
If the file is larger than available memory, this will crash.await taskcluster.aio.download.downloadArtifactToFile(taskId=.., runId=.., name=.., maxRetries=.., queueService=.., file=..)- asynchronously download an object to a standard Python file, returning the content type.await taskcluster.aio.download.downloadArtifact(taskId=.., runId=.., name=.., maxRetries=.., queueService=.., writerFactory=..)- asynchronously download an object to an async writer factory, returning the content type.taskcluster.download.downloadArtifactToBuf(taskId=.., runId=.., name=.., maxRetries=.., queueService=..)- download an object to an in-memory buffer, returning a tuple (buffer, content-type).
If the file is larger than available memory, this will crash.taskcluster.download.downloadArtifactToFile(taskId=.., runId=.., name=.., maxRetries=.., queueService=.., file=..)- download an object to a standard Python file, returning the content type.taskcluster.download.downloadArtifact(taskId=.., runId=.., name=.., maxRetries=.., queueService=.., writerFactory=..)- download an object to a sync writer factory, returning the content type.Integration HelpersThe Python Taskcluster client has a moduletaskcluster.helperwith utilities which allows you to easily share authentication options across multiple services in your project.Generally a project using this library will face different use cases and authentication options:No authentication for a new contributor without Taskcluster access,Specific client credentials through environment variables on a developer's computer,Taskcluster Proxy when running inside a task.Shared authenticationThe classtaskcluster.helper.TaskclusterConfigis made to be instantiated once in your project, usually in a top level module. That singleton is then accessed by different parts of your projects, whenever a Taskcluster service is needed.Here is a sample usage:inproject/__init__.py, no call to Taskcluster is made at that point:fromtaskcluster.helperimportTaskclusterconfigtc=TaskclusterConfig('https://community-tc.services.mozilla.com')inproject/boot.py, we authenticate on Taskcuster with provided credentials, or environment variables, or taskcluster proxy (in that order):fromprojectimporttctc.auth(client_id='XXX',access_token='YYY')at that point, you can load any service using the authenticated wrapper from anywhere in your code:fromprojectimporttcdefsync_usage():queue=tc.get_service('queue')queue.ping()asyncdefasync_usage():hooks=tc.get_service('hooks',use_async=True)# Asynchronous service classawaithooks.ping()Supported environment variables are:TASKCLUSTER_ROOT_URLto specify your Taskcluster instance base url. You can either use that variable or instanciateTaskclusterConfigwith the base url.TASKCLUSTER_CLIENT_ID&TASKCLUSTER_ACCESS_TOKENto specify your client credentials instead of providing them toTaskclusterConfig.authTASKCLUSTER_PROXY_URLto specify the proxy address used to reach Taskcluster in a task. It defaults tohttp://taskclusterwhen not specified.For more details on Taskcluster environment variables,here is the documentation.Loading secrets across multiple authenticationsAnother available utility istaskcluster.helper.load_secretswhich allows you to retrieve a secret using an authenticatedtaskcluster.Secretsinstance (usingTaskclusterConfig.get_serviceor the synchronous class directly).This utility loads a secret, but allows you to:share a secret across multiple projects, by using key prefixes inside the secret,check that some required keys are present in the secret,provide some default values,provide a local secret source instead of using the Taskcluster service (useful for local development or sharingsecretswith contributors)Let's say you have a secret on a Taskcluster instance namedproject/foo/prod-config, which is needed by a backend and some tasks. Here is its content:common:environment:productionremote_log:https://log.xx.com/payloadbackend:bugzilla_token:XXXXtask:backend_url:https://backend.foo.mozilla.comIn your backend, you would do:fromtaskclusterimportSecretsfromtaskcluster.helperimportload_secretsprod_config=load_secrets(Secrets({...}),'project/foo/prod-config',# We only need the common & backend partsprefixes=['common','backend'],# We absolutely need a bugzilla token to runrequired=['bugzilla_token'],# Let's provide some default value for the environmentexisting={'environment':'dev',})# -> prod_config == {#     "environment": "production"#     "remote_log": "https://log.xx.com/payload",#     "bugzilla_token": "XXXX",#   }In your task, you could do the following usingTaskclusterConfigmentionned above (the class has a shortcut to use an authenticatedSecretsservice automatically):fromprojectimporttcprod_config=tc.load_secrets('project/foo/prod-config',# We only need the common & bot partsprefixes=['common','bot'],# Let's provide some default value for the environment and backend_urlexisting={'environment':'dev','backend_url':'http://localhost:8000',})# -> prod_config == {#     "environment": "production"#     "remote_log": "https://log.xx.com/payload",#     "backend_url": "https://backend.foo.mozilla.com",#   }To provide local secrets value, you first need to load these values as a dictionary (usually by reading a local file in your format of choice : YAML, JSON, ...) and providing the dictionary toload_secretsby using thelocal_secretsparameter:importosimportyamlfromtaskclusterimportSecretsfromtaskcluster.helperimportload_secretslocal_path='path/to/file.yml'prod_config=load_secrets(Secrets({...}),'project/foo/prod-config',# We support an optional local file to provide some configuration without reaching Taskclusterlocal_secrets=yaml.safe_load(open(local_path))ifos.path.exists(local_path)elseNone,)CompatibilityThis library is co-versioned with Taskcluster itself.
That is, a client with version x.y.z contains API methods corresponding to Taskcluster version x.y.z.
Taskcluster is careful to maintain API compatibility, and guarantees it within a major version.
That means that any client with version x.* will work against any Taskcluster services at version x.*, and is very likely to work for many other major versions of the Taskcluster services.
Any incompatibilities are noted in theChangelog.