introduction.rst 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. ************
  2. launchpadlib
  3. ************
  4. launchpadlib is the standalone Python language bindings to Launchpad's web
  5. services API. It is officially supported by Canonical, although third party
  6. packages may be available to provide bindings to other programming languages.
  7. Set up
  8. ======
  9. launchpadlib writes to $HOME, so isolate ourselves.
  10. >>> from fixtures import (
  11. ... EnvironmentVariable,
  12. ... TempDir,
  13. ... )
  14. >>> tempdir_fixture = TempDir()
  15. >>> tempdir_fixture.setUp()
  16. >>> home_fixture = EnvironmentVariable('HOME', tempdir_fixture.path)
  17. >>> home_fixture.setUp()
  18. OAuth authentication
  19. ====================
  20. The Launchpad API requires user authentication via OAuth, and launchpadlib
  21. provides a high level interface to OAuth for the most common use cases.
  22. Several pieces of information are necessary to complete the OAuth request:
  23. * A consumer key, which is unique to the application using the API
  24. * An access token, which represents the user to the web service
  25. * An access token secret, essentially a password for the token
  26. Consumer keys are hard-baked into the application. They are generated by the
  27. application developer and registered with Launchpad independently of the use
  28. of the application. Since consumer keys are arbitrary, a registered consumer
  29. key can be paired with a secret, but most open source applications will forgo
  30. this since it's not really a secret anyway.
  31. The access token cannot be provided directly. Instead, the application
  32. generates an unauthenticated request token, exchanging this for an access
  33. token and a secret after obtaining approval to do so from the user. This
  34. permission is typically gained by redirecting the user through their trusted
  35. web browser, then back to the application.
  36. This entire exchange is managed by launchpadlib's credentials classes.
  37. Credentials can be stored in a file, though the security of this depends on
  38. the implementation of the file object. In the simplest case, the application
  39. will request a new access token every time.
  40. >>> from launchpadlib.credentials import Consumer
  41. >>> consumer = Consumer('launchpad-library')
  42. >>> consumer.key
  43. 'launchpad-library'
  44. >>> consumer.secret
  45. ''
  46. Salgado has full access to the Launchpad API. Out of band, the application
  47. itself obtains Salgado's approval to access the Launchpad API on his behalf.
  48. How the application does this is up to the application, provided it conforms
  49. to the OAuth protocol. Once this happens, we have Salgado's credentials for
  50. accessing Launchpad.
  51. >>> from launchpadlib.credentials import AccessToken
  52. >>> access_token = AccessToken('salgado-change-anything', 'test')
  53. And now these credentials are used to access the root service on Salgado's
  54. behalf.
  55. >>> from launchpadlib.credentials import Credentials
  56. >>> credentials = Credentials(
  57. ... consumer_name=consumer.key, consumer_secret=consumer.secret,
  58. ... access_token=access_token)
  59. >>> from launchpadlib.testing.helpers import (
  60. ... TestableLaunchpad as Launchpad)
  61. >>> launchpad = Launchpad(credentials=credentials)
  62. >>> list(launchpad.people)
  63. [...]
  64. >>> list(launchpad.bugs)
  65. [...]
  66. If available, the Gnome keyring or KDE wallet will be used to store access
  67. tokens. If a keyring/wallet is not available, the application can store the
  68. credentials on the file system, so that the next time Salgado interacts with
  69. the application, he won't have to go through the whole OAuth request dance.
  70. >>> import os
  71. >>> import tempfile
  72. >>> fd, path = tempfile.mkstemp('.credentials')
  73. >>> os.close(fd)
  74. Once Salgado's credentials are obtained for the first time, just set the
  75. appropriate instance variables and use the save() method.
  76. >>> credentials.consumer = consumer
  77. >>> credentials.access_token = access_token
  78. >>> credentials_file = open(path, 'w')
  79. >>> credentials.save(credentials_file)
  80. >>> credentials_file.close()
  81. And the credentials are perfectly valid for accessing Launchpad.
  82. >>> launchpad = Launchpad(credentials=credentials)
  83. >>> list(launchpad.people)
  84. [...]
  85. >>> list(launchpad.bugs)
  86. [...]
  87. The credentials can also be retrieved from the file, so that the OAuth request
  88. dance can be avoided.
  89. >>> credentials = Credentials()
  90. >>> credentials_file = open(path)
  91. >>> credentials.load(credentials_file)
  92. >>> credentials_file.close()
  93. >>> credentials.consumer.key
  94. 'launchpad-library'
  95. >>> credentials.consumer.secret
  96. ''
  97. >>> credentials.access_token.key
  98. 'salgado-change-anything'
  99. >>> credentials.access_token.secret
  100. 'test'
  101. These credentials too, are perfectly usable to access Launchpad.
  102. >>> launchpad = Launchpad(credentials=credentials)
  103. >>> list(launchpad.people)
  104. [...]
  105. >>> list(launchpad.bugs)
  106. [...]
  107. The security of the stored credentials is left up to the file-like object.
  108. Here, the application decides to use a dubious encryption algorithm to hide
  109. Salgado's credentials.
  110. >>> import io
  111. >>> from codecs import EncodedFile
  112. >>> encrypted_file = io.BytesIO()
  113. >>> stream = EncodedFile(encrypted_file, 'rot_13', 'ascii')
  114. >>> credentials.save(stream)
  115. >>> _ = stream.seek(0, 0)
  116. >>> print(''.join(sorted([line.decode() for line in encrypted_file])))
  117. [1]
  118. <BLANKLINE>
  119. <BLANKLINE>
  120. npprff_frperg = grfg
  121. npprff_gbxra = fnytnqb-punatr-nalguvat
  122. pbafhzre_frperg =
  123. pbafhzre_xrl = ynhapucnq-yvoenel
  124. >>> _ = stream.seek(0)
  125. >>> credentials = Credentials()
  126. >>> credentials.load(stream)
  127. >>> credentials.consumer.key
  128. 'launchpad-library'
  129. >>> credentials.consumer.secret
  130. ''
  131. >>> credentials.access_token.key
  132. 'salgado-change-anything'
  133. >>> credentials.access_token.secret
  134. 'test'
  135. Anonymous access
  136. ================
  137. An anonymous access token doesn't authenticate any particular
  138. user. Using it will give a client read-only access to the public parts
  139. of the Launchpad dataset.
  140. >>> from launchpadlib.credentials import AnonymousAccessToken
  141. >>> anonymous_token = AnonymousAccessToken()
  142. >>> from launchpadlib.credentials import Credentials
  143. >>> credentials = Credentials(
  144. ... consumer_name="a consumer", access_token=anonymous_token)
  145. >>> launchpad = Launchpad(credentials=credentials)
  146. >>> salgado = launchpad.people['salgado']
  147. >>> print(salgado.display_name)
  148. Guilherme Salgado
  149. An anonymous client can't modify the dataset, or read any data that's
  150. permission-controlled or scoped to a particular user.
  151. >>> launchpad.me
  152. Traceback (most recent call last):
  153. ...
  154. lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
  155. ...
  156. >>> salgado.display_name = "This won't work."
  157. >>> salgado.lp_save()
  158. Traceback (most recent call last):
  159. ...
  160. lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
  161. ...
  162. Convenience
  163. ===========
  164. When you want anonymous access, a convenience method is available for
  165. setting up a web service connection in one function call. All you have
  166. to provide is the consumer name.
  167. >>> launchpad = Launchpad.login_anonymously(
  168. ... 'launchpad-library', service_root="test_dev")
  169. >>> list(launchpad.people)
  170. [...]
  171. >>> launchpad.me
  172. Traceback (most recent call last):
  173. ...
  174. lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
  175. ...
  176. Otherwise, the application should obtain authorization from the user
  177. and get a new set of credentials directly from
  178. Launchpad.
  179. Unfortunately, we can't test this entire process because it requires
  180. opening up a web browser, but we can test the first step, which is to
  181. get a request token.
  182. >>> import launchpadlib.credentials
  183. >>> credentials = Credentials('consumer')
  184. >>> authorization_url = credentials.get_request_token(
  185. ... context='firefox', web_root='test_dev')
  186. >>> print(authorization_url)
  187. http://launchpad.test:8085/+authorize-token?oauth_token=...&lp.context=firefox
  188. We use 'test_dev' as a shorthand for the root URL of the Launchpad
  189. installation. It's defined in the 'uris' module as
  190. 'http://launchpad.test:8085/', and the launchpadlib code knows how to
  191. dereference it before using it as a URL.
  192. Information about the request token is kept in the _request_token
  193. attribute of the Credentials object.
  194. >>> credentials._request_token.key is not None
  195. True
  196. >>> credentials._request_token.secret is not None
  197. True
  198. >>> print(credentials._request_token.context)
  199. firefox
  200. Now the user must authorize that token, and this is the part we can't
  201. test--it requires opening a web browser. Once the token is authorized
  202. on the server side, we can call exchange_request_token_for_access_token()
  203. on our Credentials object, which will then be ready to use.
  204. The dictionary request token
  205. ============================
  206. By default, get_request_token returns the URL that the user needs to
  207. use when granting access to the token. But you can specify a different
  208. token_format and get a dictionary instead.
  209. >>> credentials = Credentials('consumer')
  210. >>> dictionary = credentials.get_request_token(
  211. ... context='firefox', web_root='test_dev',
  212. ... token_format=Credentials.DICT_TOKEN_FORMAT)
  213. The dictionary has useful information about the token and about the
  214. levels of authentication Launchpad offers.
  215. >>> for param in sorted(dictionary.keys()):
  216. ... print(param)
  217. access_levels
  218. lp.context
  219. oauth_token
  220. oauth_token_consumer
  221. oauth_token_secret
  222. The _request_token attribute of the Credentials object has the same
  223. fields set as if you had asked for the default URI token format.
  224. >>> credentials._request_token.key is not None
  225. True
  226. >>> credentials._request_token.secret is not None
  227. True
  228. >>> print(credentials._request_token.context)
  229. firefox
  230. Credentials file errors
  231. =======================
  232. If the credentials file is empty, loading it raises an exception.
  233. >>> credentials = Credentials()
  234. >>> credentials.load(io.StringIO())
  235. Traceback (most recent call last):
  236. ...
  237. lazr.restfulclient.errors.CredentialsFileError: No configuration for
  238. version 1
  239. It is an error to save a credentials file when no consumer or access token is
  240. available.
  241. >>> credentials.consumer = None
  242. >>> credentials.save(io.StringIO())
  243. Traceback (most recent call last):
  244. ...
  245. lazr.restfulclient.errors.CredentialsFileError: No consumer
  246. >>> credentials.consumer = consumer
  247. >>> credentials.access_token = None
  248. >>> credentials.save(io.StringIO())
  249. Traceback (most recent call last):
  250. ...
  251. lazr.restfulclient.errors.CredentialsFileError: No access token
  252. The credentials file is not intended to be edited, but because it's human
  253. readable, that's of course possible. If the credentials file gets corrupted,
  254. an error is raised.
  255. >>> credentials_file = io.StringIO("""\
  256. ... [1]
  257. ... #consumer_key: aardvark
  258. ... consumer_secret: badger
  259. ... access_token: caribou
  260. ... access_secret: dingo
  261. ... """)
  262. >>> credentials.load(credentials_file)
  263. Traceback (most recent call last):
  264. ...
  265. configparser.NoOptionError: No option 'consumer_key' in section: '1'
  266. >>> credentials_file = io.StringIO("""\
  267. ... [1]
  268. ... consumer_key: aardvark
  269. ... #consumer_secret: badger
  270. ... access_token: caribou
  271. ... access_secret: dingo
  272. ... """)
  273. >>> credentials.load(credentials_file)
  274. Traceback (most recent call last):
  275. ...
  276. configparser.NoOptionError: No option 'consumer_secret' in section: '1'
  277. >>> credentials_file = io.StringIO("""\
  278. ... [1]
  279. ... consumer_key: aardvark
  280. ... consumer_secret: badger
  281. ... #access_token: caribou
  282. ... access_secret: dingo
  283. ... """)
  284. >>> credentials.load(credentials_file)
  285. Traceback (most recent call last):
  286. ...
  287. configparser.NoOptionError: No option 'access_token' in section: '1'
  288. >>> credentials_file = io.StringIO("""\
  289. ... [1]
  290. ... consumer_key: aardvark
  291. ... consumer_secret: badger
  292. ... access_token: caribou
  293. ... #access_secret: dingo
  294. ... """)
  295. >>> credentials.load(credentials_file)
  296. Traceback (most recent call last):
  297. ...
  298. configparser.NoOptionError: No option 'access_secret' in section: '1'
  299. Bad credentials
  300. ===============
  301. The application is not allowed to access Launchpad with a bad access token.
  302. >>> access_token = AccessToken('bad', 'no-secret')
  303. >>> credentials = Credentials(
  304. ... consumer_name=consumer.key, consumer_secret=consumer.secret,
  305. ... access_token=access_token)
  306. >>> launchpad = Launchpad(credentials=credentials)
  307. Traceback (most recent call last):
  308. ...
  309. lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
  310. ...
  311. The application is not allowed to access Launchpad with a consumer
  312. name that doesn't match the credentials.
  313. >>> access_token = AccessToken('salgado-change-anything', 'test')
  314. >>> credentials = Credentials(
  315. ... consumer_name='not-the-launchpad-library',
  316. ... access_token=access_token)
  317. >>> launchpad = Launchpad(credentials=credentials)
  318. Traceback (most recent call last):
  319. ...
  320. lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
  321. ...
  322. The application is not allowed to access Launchpad with a bad access secret.
  323. >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret')
  324. >>> credentials = Credentials(
  325. ... consumer_name=consumer.key, consumer_secret=consumer.secret,
  326. ... access_token=access_token)
  327. >>> launchpad = Launchpad(credentials=credentials)
  328. Traceback (most recent call last):
  329. ...
  330. lazr.restfulclient.errors.Unauthorized: HTTP Error 401: Unauthorized
  331. ...
  332. Clean up
  333. ========
  334. >>> os.remove(path)
  335. >>> home_fixture.cleanUp()
  336. >>> tempdir_fixture.cleanUp()