= launchpadlib =

launchpadlib is the standalone Python language bindings to Launchpad's web
services API.  It is officially supported by Canonical, although third party
packages may be available to provide bindings to other programming languages.

launchpadlib is currently in alpha release.

    >>> import launchpadlib
    >>> launchpadlib.__version__
    '0.2'


== OAuth authentication ==

The Launchpad API requires user authentication via OAuth, and launchpadlib
provides a high level interface to OAuth for the most common use cases.
Several pieces of information are necessary to complete the OAuth request:

 * A consumer key, which is unique to the application using the API
 * An access token, which represents the user to the web service
 * An access token secret, essentially a password for the token

Consumer keys are hard-baked into the application.  They are generated by the
application developer and registered with Launchpad independently of the use
of the application.  Since consumer keys are arbitrary, a registered consumer
key can be paired with a secret, but most open source applications will forgo
this since it's not really a secret anyway.

The access token cannot be provided directly.  Instead, the application
generates an unauthenticated request token, exchanging this for an access
token and a secret after obtaining approval to do so from the user.  This
permission is typically gained by redirecting the user through their trusted
web browser, then back to the application.

This entire exchange is managed by launchpadlib's credentials classes.
Credentials can be stored in a file, though the security of this depends on
the implementation of the file object.  In the simplest case, the application
will request a new access token every time.

    >>> from launchpadlib.credentials import Consumer
    >>> consumer = Consumer('launchpad-library')
    >>> consumer.key
    'launchpad-library'
    >>> consumer.secret
    ''

Salgado has full access to the Launchpad API.  Out of band, the application
itself obtains Salgado's approval to access the Launchpad API on his behalf.
How the application does this is up to the application, provided it conforms
to the OAuth protocol.  Once this happens, we have Salgado's credentials for
accessing Launchpad.

    >>> from launchpadlib.credentials import AccessToken
    >>> access_token = AccessToken('salgado-change-anything', 'test')

And now these credentials are used to access the root service on Salgado's
behalf.

    >>> from launchpadlib.credentials import Credentials
    >>> credentials = Credentials(
    ...     consumer_name=consumer.key, consumer_secret=consumer.secret,
    ...     access_token=access_token)

    >>> from launchpadlib.testing.helpers import (
    ...     TestableLaunchpad as Launchpad)
    >>> launchpad = Launchpad(credentials=credentials)
    >>> sorted(launchpad.people)
    [...]
    >>> sorted(launchpad.bugs)
    [...]

For convenience, the application may store the credentials on the file system,
so that the next time Salgado interacts with the application, he won't have
to go through the whole OAuth request dance.

    >>> import os
    >>> import tempfile
    >>> fd, path = tempfile.mkstemp('.credentials')
    >>> os.close(fd)

Once Salgado's credentials are obtained for the first time, just set the
appropriate instance variables and use the save() method.

    >>> credentials.consumer = consumer
    >>> credentials.access_token = access_token
    >>> credentials_file = open(path, 'w')
    >>> credentials.save(credentials_file)
    >>> credentials_file.close()

And the credentials are perfectly valid for accessing Launchpad.

    >>> launchpad = Launchpad(credentials=credentials)
    >>> sorted(launchpad.people)
    [...]
    >>> sorted(launchpad.bugs)
    [...]

The credentials can also be retrieved from the file, so that the OAuth request
dance can be avoided.

    >>> credentials = Credentials()
    >>> credentials_file = open(path)
    >>> credentials.load(credentials_file)
    >>> credentials_file.close()
    >>> credentials.consumer.key
    'launchpad-library'
    >>> credentials.consumer.secret
    ''
    >>> credentials.access_token.key
    'salgado-change-anything'
    >>> credentials.access_token.secret
    'test'

These credentials too, are perfectly usable to access Launchpad.

    >>> launchpad = Launchpad(credentials=credentials)
    >>> sorted(launchpad.people)
    [...]
    >>> sorted(launchpad.bugs)
    [...]

The security of the stored credentials is left up to the file-like object.
Here, the application decides to use a dubious encryption algorithm to hide
Salgado's credentials.

    >>> from StringIO import StringIO
    >>> from codecs import EncodedFile
    >>> encrypted_file = StringIO()
    >>> stream = EncodedFile(encrypted_file, 'rot_13', 'ascii')
    >>> credentials.save(stream)
    >>> print encrypted_file.getvalue()
    [1]
    pbafhzre_frperg =
    npprff_gbxra = fnytnqb-punatr-nalguvat
    pbafhzre_xrl = ynhapucnq-yvoenel
    npprff_frperg = grfg
    <BLANKLINE>
    <BLANKLINE>

    >>> stream.seek(0)
    >>> credentials = Credentials()
    >>> credentials.load(stream)
    >>> credentials.consumer.key
    'launchpad-library'
    >>> credentials.consumer.secret
    ''
    >>> credentials.access_token.key
    'salgado-change-anything'
    >>> credentials.access_token.secret
    'test'


== Convenience ==

When the consumer name, access token and access secret are all known up-front,
a convenience method is available for logging into the web service in one
function call.

    >>> launchpad = Launchpad.login(
    ...     'launchpad-library', 'salgado-change-anything', 'test')
    >>> sorted(launchpad.people)
    [...]

If that is not the case the application should obtain authorization from
the user and get the credentials directly from Launchpad.

First we must get a request token.

    >>> import launchpadlib.credentials
    >>> credentials = Credentials('consumer')
    >>> authorization_url = credentials.get_request_token(
    ...     context='firefox', web_root='http://launchpad.dev:8085/')
    >>> authorization_url
    'http://launchpad.dev:8085/+authorize-token?oauth_token=...&lp.context=firefox'

Now the user must authorize that token, so we'll hand-craft a request
to pretend the user is authorizing it.

    >>> import httplib2
    >>> from urllib import urlencode
    >>> params = {'field.actions.WRITE_PRIVATE': 1,
    ...           'oauth_token': credentials._request_token.key,
    ...           'lp.context': 'firefox'}
    >>> foo_bar_auth = 'Basic %s' % (
    ...     'foo.bar@canonical.com:test'.encode('base64'))
    >>> headers = {'Authorization': foo_bar_auth}
    >>> response, content = httplib2.Http().request(
    ...     authorization_url, method='POST', body=urlencode(params),
    ...     headers=headers)
    >>> response['status']
    '200'

After that we can exchange that request token for an access token.

    >>> credentials.exchange_request_token_for_access_token(
    ...     web_root='http://launchpad.dev:8085/')

Once that's done, our credentials will be complete and ready to use.

    >>> credentials.consumer.key
    'consumer'
    >>> credentials.access_token
    <launchpadlib.credentials.AccessToken...
    >>> credentials.access_token.key is not None
    True
    >>> credentials.access_token.secret is not None
    True
    >>> credentials.access_token.context
    'firefox'

There's also a convenience method which does the access token
negotiation and logs into the web service.  It uses the methods
documented above and once it has the request token's authorization URL
it opens up a web browser for the user to authoriza it and asks him to
come back and press <Enter> once that's done.  When he does it, the
request token is exchanged for an access token and the authentication is
complete.

    # Since this will open up a web browser we're not going to actually run it
    # here.
    >>> # consumer_name = 'launchpadlib'
    >>> # launchpad = Launchpad.get_token_and_login(consumer_name)


== Credentials file errors ==

If the credentials file is empty, loading it raises an exception.

    >>> credentials = Credentials()
    >>> credentials.load(StringIO())
    Traceback (most recent call last):
    ...
    CredentialsFileError: No configuration for version 1

It is an error to save a credentials file when no consumer or access token is
available.

    >>> credentials.consumer = None
    >>> credentials.save(StringIO())
    Traceback (most recent call last):
    ...
    CredentialsFileError: No consumer

    >>> credentials.consumer = consumer
    >>> credentials.access_token = None
    >>> credentials.save(StringIO())
    Traceback (most recent call last):
    ...
    CredentialsFileError: No access token

The credentials file is not intended to be edited, but because it's human
readable, that's of course possible.  If the credentials file gets corrupted,
an error is raised.

    >>> credentials_file = StringIO("""\
    ... [1]
    ... #consumer_key: aardvark
    ... consumer_secret: badger
    ... access_token: caribou
    ... access_secret: dingo
    ... """)
    >>> credentials.load(credentials_file)
    Traceback (most recent call last):
    ...
    NoOptionError: No option 'consumer_key' in section: '1'

    >>> credentials_file = StringIO("""\
    ... [1]
    ... consumer_key: aardvark
    ... #consumer_secret: badger
    ... access_token: caribou
    ... access_secret: dingo
    ... """)
    >>> credentials.load(credentials_file)
    Traceback (most recent call last):
    ...
    NoOptionError: No option 'consumer_secret' in section: '1'

    >>> credentials_file = StringIO("""\
    ... [1]
    ... consumer_key: aardvark
    ... consumer_secret: badger
    ... #access_token: caribou
    ... access_secret: dingo
    ... """)
    >>> credentials.load(credentials_file)
    Traceback (most recent call last):
    ...
    NoOptionError: No option 'access_token' in section: '1'

    >>> credentials_file = StringIO("""\
    ... [1]
    ... consumer_key: aardvark
    ... consumer_secret: badger
    ... access_token: caribou
    ... #access_secret: dingo
    ... """)
    >>> credentials.load(credentials_file)
    Traceback (most recent call last):
    ...
    NoOptionError: No option 'access_secret' in section: '1'


== Bad credentials ==

The application is not allowed to access Launchpad if there are no
credentials.

    >>> credentials = Credentials(consumer)
    >>> launchpad = Launchpad(credentials=credentials)
    Traceback (most recent call last):
    ...
    HTTPError: HTTP Error 401: Unauthorized

The application is not allowed to access Launchpad with a bad access token.

    >>> access_token = AccessToken('bad', 'no-secret')
    >>> credentials = Credentials(
    ...     consumer_name=consumer.key, consumer_secret=consumer.secret,
    ...     access_token=access_token)
    >>> launchpad = Launchpad(credentials=credentials)
    Traceback (most recent call last):
    ...
    HTTPError: HTTP Error 401: Unauthorized

The application is not allowed to access Launchpad with a bad access secret.

    >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret')
    >>> credentials = Credentials(
    ...     consumer_name=consumer.key, consumer_secret=consumer.secret,
    ...     access_token=access_token)
    >>> launchpad = Launchpad(credentials=credentials)
    Traceback (most recent call last):
    ...
    HTTPError: HTTP Error 401: Unauthorized


== Clean up ==

    >>> os.remove(path)
