123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- ************
- 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.
- Set up
- ======
- launchpadlib writes to $HOME, so isolate ourselves.
- >>> from fixtures import (
- ... EnvironmentVariable,
- ... TempDir,
- ... )
- >>> tempdir_fixture = TempDir()
- >>> tempdir_fixture.setUp()
- >>> home_fixture = EnvironmentVariable('HOME', tempdir_fixture.path)
- >>> home_fixture.setUp()
- 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)
- >>> list(launchpad.people)
- [...]
- >>> list(launchpad.bugs)
- [...]
- If available, the Gnome keyring or KDE wallet will be used to store access
- tokens. If a keyring/wallet is not available, the application can 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)
- >>> list(launchpad.people)
- [...]
- >>> list(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)
- >>> list(launchpad.people)
- [...]
- >>> list(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.
- >>> import io
- >>> from codecs import EncodedFile
- >>> encrypted_file = io.BytesIO()
- >>> stream = EncodedFile(encrypted_file, 'rot_13', 'ascii')
- >>> credentials.save(stream)
- >>> _ = stream.seek(0, 0)
- >>> print(''.join(sorted([line.decode() for line in encrypted_file])))
- [1]
- <BLANKLINE>
- <BLANKLINE>
- npprff_frperg = grfg
- npprff_gbxra = fnytnqb-punatr-nalguvat
- pbafhzre_frperg =
- pbafhzre_xrl = ynhapucnq-yvoenel
- >>> _ = 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'
- Anonymous access
- ================
- An anonymous access token doesn't authenticate any particular
- user. Using it will give a client read-only access to the public parts
- of the Launchpad dataset.
- >>> from launchpadlib.credentials import AnonymousAccessToken
- >>> anonymous_token = AnonymousAccessToken()
- >>> from launchpadlib.credentials import Credentials
- >>> credentials = Credentials(
- ... consumer_name="a consumer", access_token=anonymous_token)
- >>> launchpad = Launchpad(credentials=credentials)
- >>> salgado = launchpad.people['salgado']
- >>> print(salgado.display_name)
- Guilherme Salgado
- An anonymous client can't modify the dataset, or read any data that's
- permission-controlled or scoped to a particular user.
- >>> launchpad.me
- Traceback (most recent call last):
- ...
- lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
- ...
- >>> salgado.display_name = "This won't work."
- >>> salgado.lp_save()
- Traceback (most recent call last):
- ...
- lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
- ...
- Convenience
- ===========
- When you want anonymous access, a convenience method is available for
- setting up a web service connection in one function call. All you have
- to provide is the consumer name.
- >>> launchpad = Launchpad.login_anonymously(
- ... 'launchpad-library', service_root="test_dev")
- >>> list(launchpad.people)
- [...]
- >>> launchpad.me
- Traceback (most recent call last):
- ...
- lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
- ...
- Otherwise, the application should obtain authorization from the user
- and get a new set of credentials directly from
- Launchpad.
- Unfortunately, we can't test this entire process because it requires
- opening up a web browser, but we can test the first step, which is to
- get a request token.
- >>> import launchpadlib.credentials
- >>> credentials = Credentials('consumer')
- >>> authorization_url = credentials.get_request_token(
- ... context='firefox', web_root='test_dev')
- >>> print(authorization_url)
- http://launchpad.test:8085/+authorize-token?oauth_token=...&lp.context=firefox
- We use 'test_dev' as a shorthand for the root URL of the Launchpad
- installation. It's defined in the 'uris' module as
- 'http://launchpad.test:8085/', and the launchpadlib code knows how to
- dereference it before using it as a URL.
- Information about the request token is kept in the _request_token
- attribute of the Credentials object.
- >>> credentials._request_token.key is not None
- True
- >>> credentials._request_token.secret is not None
- True
- >>> print(credentials._request_token.context)
- firefox
- Now the user must authorize that token, and this is the part we can't
- test--it requires opening a web browser. Once the token is authorized
- on the server side, we can call exchange_request_token_for_access_token()
- on our Credentials object, which will then be ready to use.
- The dictionary request token
- ============================
- By default, get_request_token returns the URL that the user needs to
- use when granting access to the token. But you can specify a different
- token_format and get a dictionary instead.
- >>> credentials = Credentials('consumer')
- >>> dictionary = credentials.get_request_token(
- ... context='firefox', web_root='test_dev',
- ... token_format=Credentials.DICT_TOKEN_FORMAT)
- The dictionary has useful information about the token and about the
- levels of authentication Launchpad offers.
- >>> for param in sorted(dictionary.keys()):
- ... print(param)
- access_levels
- lp.context
- oauth_token
- oauth_token_consumer
- oauth_token_secret
- The _request_token attribute of the Credentials object has the same
- fields set as if you had asked for the default URI token format.
- >>> credentials._request_token.key is not None
- True
- >>> credentials._request_token.secret is not None
- True
- >>> print(credentials._request_token.context)
- firefox
- Credentials file errors
- =======================
- If the credentials file is empty, loading it raises an exception.
- >>> credentials = Credentials()
- >>> credentials.load(io.StringIO())
- Traceback (most recent call last):
- ...
- lazr.restfulclient.errors.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(io.StringIO())
- Traceback (most recent call last):
- ...
- lazr.restfulclient.errors.CredentialsFileError: No consumer
- >>> credentials.consumer = consumer
- >>> credentials.access_token = None
- >>> credentials.save(io.StringIO())
- Traceback (most recent call last):
- ...
- lazr.restfulclient.errors.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 = io.StringIO("""\
- ... [1]
- ... #consumer_key: aardvark
- ... consumer_secret: badger
- ... access_token: caribou
- ... access_secret: dingo
- ... """)
- >>> credentials.load(credentials_file)
- Traceback (most recent call last):
- ...
- configparser.NoOptionError: No option 'consumer_key' in section: '1'
- >>> credentials_file = io.StringIO("""\
- ... [1]
- ... consumer_key: aardvark
- ... #consumer_secret: badger
- ... access_token: caribou
- ... access_secret: dingo
- ... """)
- >>> credentials.load(credentials_file)
- Traceback (most recent call last):
- ...
- configparser.NoOptionError: No option 'consumer_secret' in section: '1'
- >>> credentials_file = io.StringIO("""\
- ... [1]
- ... consumer_key: aardvark
- ... consumer_secret: badger
- ... #access_token: caribou
- ... access_secret: dingo
- ... """)
- >>> credentials.load(credentials_file)
- Traceback (most recent call last):
- ...
- configparser.NoOptionError: No option 'access_token' in section: '1'
- >>> credentials_file = io.StringIO("""\
- ... [1]
- ... consumer_key: aardvark
- ... consumer_secret: badger
- ... access_token: caribou
- ... #access_secret: dingo
- ... """)
- >>> credentials.load(credentials_file)
- Traceback (most recent call last):
- ...
- configparser.NoOptionError: No option 'access_secret' in section: '1'
- Bad credentials
- ===============
- 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):
- ...
- lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
- ...
- The application is not allowed to access Launchpad with a consumer
- name that doesn't match the credentials.
- >>> access_token = AccessToken('salgado-change-anything', 'test')
- >>> credentials = Credentials(
- ... consumer_name='not-the-launchpad-library',
- ... access_token=access_token)
- >>> launchpad = Launchpad(credentials=credentials)
- Traceback (most recent call last):
- ...
- lazr.restfulclient.errors.Unauthorized: 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):
- ...
- lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
- ...
- Clean up
- ========
- >>> os.remove(path)
- >>> home_fixture.cleanUp()
- >>> tempdir_fixture.cleanUp()
|