PyPI OAuth

This page describes PyPI's OAuth 1.0a facility. To use this facility you must first contact the PyPI maintainers (Richard or Martin) to obtain a consumer key and secret pair.

The OAuth facility has been tested with the requests and rauth libraries.

All of these examples use the testpypi server rather than the production server. To use the production server:

  1. make sure that your consumer key and secret have been enabled, and

  2. modify the URLs to point to "pypi.python.org" instead of "testpypi.python.org".

Notes:

  1. the code below currently disables SSL certificate verification since the PyPI servers do not have a verifiable certificate.
  2. for the moment you need to use the very latest version of requests from the git repository (no earlier than 2012-07-27).

Obtaining an Access Token for a User

Obtaining an access token, required for all API calls, is a three-step process.

Step 1: Obtain a Request Token

Using your application/site's consumer key and secret you obtain a request token from PyPI. This will be turned into an access token once authorised by the user.

   1 import requests
   2 from requests.auth import OAuth1
   3 from urlparse import parse_qs
   4 
   5 REQUEST_TOKEN_URL = 'https://testpypi.python.org/oauth/request_token'
   6 auth = OAuth1(CONSUMER_KEY, CONSUMER_SECRET, signature_type='auth_header')
   7 response = requests.get(REQUEST_TOKEN_URL, auth=auth, verify=False)
   8 qs = parse_qs(response.text)
   9 REQUEST_TOKEN = unicode(qs['oauth_token'][0])
  10 REQUEST_SECRET = unicode(qs['oauth_token_secret'][0])

Step 2: Direct the User to Authorise Access

In this step we give the user a link or open a browser redirecting them to a PyPI web page, passing the REQUEST_TOKEN we got in the previous step as a url parameter. The user will get a dialog asking for authorisation for our application. PyPI will redirect the user back to the URL you provide in CALLBACK_URL. The callback URL passes control back to your application.

   1 import webbrowser
   2 
   3 AUTHORIZATION_URL = 'https://testpypi.python.org/oauth/authorise'
   4 CALLBACK_URL = 'http://spam.example/back'
   5 
   6 webbrowser.open("%s?oauth_token=%s&oauth_callback=%s" % (AUTHORIZATION_URL,
   7     REQUEST_TOKEN, CALLBACK_URL))
   8 
   9 raw_input('[press enter when ready]')

Step 3: Obtain the User's Authorised Access Token

Once we get user's authorization, we request a final access token, to operate on behalf of the user. We build a new hook using previous request token information achieved at step 1. The request token will have been authorized at step 2.

This code is typically invoked in the page called at CALLBACK_URL.

   1 ACCESS_TOKEN_URL = 'https://testpypi.python.org/oauth/access_token'
   2 
   3 auth = OAuth1(CONSUMER_KEY, CONSUMER_SECRET, REQUEST_TOKEN, REQUEST_SECRET,
   4     signature_type='auth_header')
   5 response = requests.get(ACCESS_TOKEN_URL, auth=auth, verify=False)
   6 response = parse_qs(response.content)
   7 ACCESS_TOKEN = unicode(response['oauth_token'][0])
   8 ACCESS_SECRET = unicode(response['oauth_token_secret'][0])

The ACCESS_TOKEN and ACCESS_SECRET are the credentials we need to use for handling user's OAuth, so most likely you will want to persist them somehow. These are the ones you should use for building a requests session with a new hook.

Testing the Access Token

Now we have an access token we may access the protected resources on behalf of the user. In this case we access the test URL which will echo back to us the authenticated user and any parameters we pass.

   1 TEST_URL = 'https://testpypi.python.org/oauth/test'
   2 
   3 auth = OAuth1(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET,
   4     signature_type='auth_header')
   5 response = requests.get(TEST_URL, params={'test': 'spam'}, auth=auth,
   6     verify=False)
   7 print response.text

PyPI OAuth API

The following code will access the PyPI OAuth API for releasing new package versions and uploading files for those releases.

   1 def flatten_params(params):
   2     '''Convert a dict to to a list of two-tuples of (k, v) where v is
   3     potentially each element from a list of values.
   4 
   5     Also ignore empty values as these confuse signature generation.
   6     '''
   7     flattened = []
   8     for k, v in params.items():
   9         if isinstance(v, list):
  10             for v in v:
  11                 if v:
  12                     flattened.append((k, v))
  13         elif v:
  14             flattened.append((k, v))
  15     return [e for e in flattened if e[1]]
  16 
  17 
  18 def release(ACCESS_TOKEN, ACCESS_SECRET, name, version, summary, **optional):
  19     '''Register a new package, or release of an existing package.
  20 
  21     The "optional" parameters match fields in PEP 345.
  22 
  23     The complete list of parameters are:
  24 
  25     Single value: description, keywords, home_page, author, author_email,
  26         maintainer, maintainer_email, license, requires_python
  27 
  28     Multiple values: requires, provides, obsoletes, requires_dist,
  29         provides_dist, obsoletes_dist, requires_external, project_url,
  30         classifiers.
  31 
  32     For parameters with multiple values, pass them as lists of strings.
  33 
  34     The API will default metadata_version to '1.2' for you. The other valid
  35     value is '1.0'.
  36 
  37     Two additional metadata fields are available specific to PyPI:
  38 
  39     1. _pypi_hidden: If set to '1' the relase will be hidden from listings and
  40        searches.
  41     2. bugtrack_url: This will be displayed on package pages.
  42     '''
  43     RESOURCE_URL = 'https://testpypi.python.org/oauth/add_release'
  44     auth = OAuth1(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET,
  45         signature_type='auth_header')
  46     params = {u'name': name, u'version': version, u'summary': summary}
  47     params.update(optional)
  48     data = flatten_params(params)
  49     response = requests.post(RESOURCE_URL, data=data, auth=auth,
  50         verify=False)
  51     return response.text
  52 
  53 
  54 def upload(ACCESS_TOKEN, ACCESS_SECRET, name, version, content,
  55         filename, filetype, **optional):
  56     '''Upload a file for a package release. If the release does not exist then
  57     it will be registered automatically.
  58 
  59     The name and version identify the package release to upload the file
  60     against. The content and filetype are specific to the file being uploaded.
  61 
  62     content - an readable file object
  63     filetype - one of the standard distutils file types ("sdist", "bdist_win",
  64         etc.)
  65 
  66     There are several optional parameters:
  67 
  68     pyversion - specify the 'N.N' Python version the distribution works with.
  69         This is not needed for source distributions but required otherwise.
  70     comment - use if there's multiple files for one distribution type.
  71     md5_digest - supply the MD5 digest of the file content to verify
  72         transmission
  73     gpg_signature - ASCII armored GPG signature for the file content
  74     protocol_version - defaults to "1" (currently the only valid value)
  75 
  76     Additionally the release parameters are as specified for release() above.
  77     '''
  78     RESOURCE_URL = 'https://testpypi.python.org/oauth/upload'
  79     auth = OAuth1(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET,
  80         signature_type='auth_header')
  81     params = dict(name=name, version=version, filename=filename,
  82         filetype=filetype, protocol_version='1')
  83     params.update(optional)
  84     data = flatten_params(params)
  85     files = dict(content=(filename, content))
  86     response = requests.post(RESOURCE_URL, params=params, files=files,
  87         auth=auth, verify=False)
  88     return response.text

So for example to register a package release, use:

   1 name = u'spam'
   2 version = u'3.3.1'
   3 summary = u'spam via OAuth'
   4 classifiers = [u'Topic :: Security', u'Environment :: Console']
   5 print 'REGISTERING', name, version
   6 print release(ACCESS_TOKEN, ACCESS_SECRET, name, version, summary,
   7     classifiers=classifiers)

And to release a file use:

   1 name = u'spam'
   2 version = u'3.3.1'
   3 filename = u'spam-3.3.1.tar.gz'
   4 filetype = u'sdist'
   5 content = open('path/to/dist/' + filename, 'rb')
   6 print 'UPLOADING', filename
   7 print upload(ACCESS_TOKEN, ACCESS_SECRET, name, version, content,
   8      filename, filetype)

Alternative rauth Implementation

Alternatively you may use the rauth library. The API presented is the same (so use the same docstrings) but internally it looks like this:

Setup

   1 from rauth.service import OAuth1Service
   2 
   3 pypi = OAuth1Service(
   4     name='pypi',
   5     consumer_key=CONSUMER_KEY,
   6     consumer_secret=CONSUMER_SECRET,
   7     request_token_url='https://testpypi.python.org/oauth/request_token',
   8     access_token_url='https://testpypi.python.org/oauth/access_token',
   9     authorize_url='https://testpypi.python.org/oauth/authorise',
  10     header_auth=True)

Obtaining an Access Token for a User

   1 REQUEST_TOKEN, REQUEST_SECRET = pypi.get_request_token(method='GET', verify=False)
   2 
   3 url = pypi.get_authorize_url(REQUEST_TOKEN, oauth_callback=CALLBACK_URL)
   4 import webbrowser
   5 webbrowser.open(url)
   6 
   7 raw_input('[press enter when ready]')
   8 
   9 response = pypi.get_access_token('GET', request_token=REQUEST_TOKEN,
  10     request_token_secret=REQUEST_SECRET, verify=False)
  11 data = response.content
  12 ACCESS_TOKEN = data['oauth_token']
  13 ACCESS_SECRET = data['oauth_token_secret']

Using the API

   1 def test(ACCESS_TOKEN, ACCESS_SECRET, **params):
   2     response = pypi.get('https://testpypi.python.org/oauth/test',
   3             params=params,
   4             access_token=ACCESS_TOKEN,
   5             access_token_secret=ACCESS_SECRET,
   6             verify=False)
   7     return response.response.content
   8 
   9 def release(ACCESS_TOKEN, ACCESS_SECRET, name, version, summary, **optional):
  10     params = {u'name': name, u'version': version, u'summary': summary}
  11     params.update(optional)
  12     data = flatten_params(params)
  13     response = pypi.post('https://testpypi.python.org/oauth/add_release',
  14             data=data, access_token=ACCESS_TOKEN,
  15             access_token_secret=ACCESS_SECRET, verify=False)
  16     return response.response.content
  17 
  18 def upload(ACCESS_TOKEN, ACCESS_SECRET, name, version, content,
  19         filename, filetype, **optional):
  20     params = dict(name=name, version=version, filename=filename,
  21         filetype=filetype, protocol_version='1')
  22     params.update(optional)
  23     data = flatten_params(params)
  24     files = dict(content=(filename, content.read()))
  25     response = pypi.post('https://testpypi.python.org/oauth/upload',
  26         params=params, files=files, access_token=ACCESS_TOKEN,
  27         access_token_secret=ACCESS_SECRET, verify=False)
  28     return response.response.content

PyPIOAuth (last edited 2013-02-20 12:34:13 by techtonik)

Unable to edit the page? See the FrontPage for instructions.