launchpad.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. # Copyright 2008-2009 Canonical Ltd.
  2. # This file is part of launchpadlib.
  3. #
  4. # launchpadlib is free software: you can redistribute it and/or modify it
  5. # under the terms of the GNU Lesser General Public License as published by the
  6. # Free Software Foundation, version 3 of the License.
  7. #
  8. # launchpadlib is distributed in the hope that it will be useful, but WITHOUT
  9. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  10. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
  11. # for more details.
  12. #
  13. # You should have received a copy of the GNU Lesser General Public License
  14. # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
  15. """Root Launchpad API class."""
  16. __metaclass__ = type
  17. __all__ = [
  18. "Launchpad",
  19. ]
  20. import errno
  21. import os
  22. try:
  23. from urllib.parse import urlsplit
  24. except ImportError:
  25. from urlparse import urlsplit
  26. import warnings
  27. try:
  28. from httplib2 import proxy_info_from_environment
  29. except ImportError:
  30. from httplib2 import ProxyInfo
  31. proxy_info_from_environment = ProxyInfo.from_environment
  32. from lazr.restfulclient.resource import ( # noqa: F401
  33. CollectionWithKeyBasedLookup,
  34. HostedFile, # Re-import for client convenience
  35. ScalarValue, # Re-import for client convenience
  36. ServiceRoot,
  37. )
  38. from lazr.restfulclient.authorize.oauth import SystemWideConsumer
  39. from lazr.restfulclient._browser import RestfulHttp
  40. from launchpadlib.credentials import (
  41. AccessToken,
  42. AnonymousAccessToken,
  43. AuthorizeRequestTokenWithBrowser,
  44. AuthorizeRequestTokenWithURL,
  45. Consumer,
  46. Credentials,
  47. MemoryCredentialStore,
  48. KeyringCredentialStore,
  49. UnencryptedFileCredentialStore,
  50. )
  51. from launchpadlib import uris
  52. # Import old constants for backwards compatibility
  53. from launchpadlib.uris import ( # noqa: F401
  54. STAGING_SERVICE_ROOT,
  55. EDGE_SERVICE_ROOT,
  56. )
  57. OAUTH_REALM = "https://api.launchpad.net"
  58. class PersonSet(CollectionWithKeyBasedLookup):
  59. """A custom subclass capable of person lookup by username."""
  60. def _get_url_from_id(self, key):
  61. """Transform a username into the URL to a person resource."""
  62. return str(self._root._root_uri.ensureSlash()) + "~" + str(key)
  63. # The only way to determine whether a string corresponds to a
  64. # person or a team object is to ask the server, so looking up an
  65. # entry from the PersonSet always requires making an HTTP request.
  66. collection_of = "team"
  67. class BugSet(CollectionWithKeyBasedLookup):
  68. """A custom subclass capable of bug lookup by bug ID."""
  69. def _get_url_from_id(self, key):
  70. """Transform a bug ID into the URL to a bug resource."""
  71. return str(self._root._root_uri.ensureSlash()) + "bugs/" + str(key)
  72. collection_of = "bug"
  73. class PillarSet(CollectionWithKeyBasedLookup):
  74. """A custom subclass capable of lookup by pillar name.
  75. Projects, project groups, and distributions are all pillars.
  76. """
  77. def _get_url_from_id(self, key):
  78. """Transform a project name into the URL to a project resource."""
  79. return str(self._root._root_uri.ensureSlash()) + str(key)
  80. # The subclasses for projects, project groups, and distributions
  81. # all define this property differently.
  82. collection_of = None
  83. class ProjectSet(PillarSet):
  84. """A custom subclass for accessing the collection of projects."""
  85. collection_of = "project"
  86. class ProjectGroupSet(PillarSet):
  87. """A custom subclass for accessing the collection of project groups."""
  88. collection_of = "project_group"
  89. class DistributionSet(PillarSet):
  90. """A custom subclass for accessing the collection of project groups."""
  91. collection_of = "distribution"
  92. class LaunchpadOAuthAwareHttp(RestfulHttp):
  93. """Detects expired/invalid OAuth tokens and tries to get a new token."""
  94. def __init__(self, launchpad, authorization_engine, *args):
  95. self.launchpad = launchpad
  96. self.authorization_engine = authorization_engine
  97. super(LaunchpadOAuthAwareHttp, self).__init__(*args)
  98. def _bad_oauth_token(self, response, content):
  99. """Helper method to detect an error caused by a bad OAuth token."""
  100. return response.status == 401 and (
  101. content.startswith(b"Expired token")
  102. or content.startswith(b"Invalid token")
  103. or content.startswith(b"Unknown access token")
  104. )
  105. def _request(self, *args):
  106. response, content = super(LaunchpadOAuthAwareHttp, self)._request(
  107. *args
  108. )
  109. return self.retry_on_bad_token(response, content, *args)
  110. def retry_on_bad_token(self, response, content, *args):
  111. """If the response indicates a bad token, get a new token and retry.
  112. Otherwise, just return the response.
  113. """
  114. if (
  115. self._bad_oauth_token(response, content)
  116. and self.authorization_engine is not None
  117. ):
  118. # This access token is bad. Scrap it and create a new one.
  119. self.launchpad.credentials.access_token = None
  120. self.authorization_engine(
  121. self.launchpad.credentials, self.launchpad.credential_store
  122. )
  123. # Retry the request with the new credentials.
  124. return self._request(*args)
  125. return response, content
  126. class Launchpad(ServiceRoot):
  127. """Root Launchpad API class.
  128. :ivar credentials: The credentials instance used to access Launchpad.
  129. :type credentials: `Credentials`
  130. """
  131. DEFAULT_VERSION = "1.0"
  132. RESOURCE_TYPE_CLASSES = {
  133. "bugs": BugSet,
  134. "distributions": DistributionSet,
  135. "people": PersonSet,
  136. "project_groups": ProjectGroupSet,
  137. "projects": ProjectSet,
  138. }
  139. RESOURCE_TYPE_CLASSES.update(ServiceRoot.RESOURCE_TYPE_CLASSES)
  140. def __init__(
  141. self,
  142. credentials,
  143. authorization_engine,
  144. credential_store,
  145. service_root=uris.STAGING_SERVICE_ROOT,
  146. cache=None,
  147. timeout=None,
  148. proxy_info=proxy_info_from_environment,
  149. version=DEFAULT_VERSION,
  150. ):
  151. """Root access to the Launchpad API.
  152. :param credentials: The credentials used to access Launchpad.
  153. :type credentials: `Credentials`
  154. :param authorization_engine: The object used to get end-user input
  155. for authorizing OAuth request tokens. Used when an OAuth
  156. access token expires or becomes invalid during a
  157. session, or is discovered to be invalid once launchpadlib
  158. starts up.
  159. :type authorization_engine: `RequestTokenAuthorizationEngine`
  160. :param service_root: The URL to the root of the web service.
  161. :type service_root: string
  162. """
  163. service_root = uris.lookup_service_root(service_root)
  164. if service_root.endswith(version) or service_root.endswith(
  165. version + "/"
  166. ):
  167. error = (
  168. "It looks like you're using a service root that "
  169. "incorporates the name of the web service version "
  170. '("%s"). Please use one of the constants from '
  171. "launchpadlib.uris instead, or at least remove "
  172. "the version name from the root URI." % version
  173. )
  174. raise ValueError(error)
  175. self.credential_store = credential_store
  176. # We already have an access token, but it might expire or
  177. # become invalid during use. Store the authorization engine in
  178. # case we need to authorize a new token during use.
  179. self.authorization_engine = authorization_engine
  180. super(Launchpad, self).__init__(
  181. credentials, service_root, cache, timeout, proxy_info, version
  182. )
  183. def httpFactory(self, credentials, cache, timeout, proxy_info):
  184. return LaunchpadOAuthAwareHttp(
  185. self,
  186. self.authorization_engine,
  187. credentials,
  188. cache,
  189. timeout,
  190. proxy_info,
  191. )
  192. @classmethod
  193. def _is_sudo(cls):
  194. return {"SUDO_USER", "SUDO_UID", "SUDO_GID"} & set(os.environ.keys())
  195. @classmethod
  196. def authorization_engine_factory(cls, *args):
  197. if cls._is_sudo():
  198. # Do not try to open browser window under sudo;
  199. # we probably don't have access to the X session,
  200. # and some browsers (e.g. chromium) won't run as root
  201. # LP: #1825014
  202. return AuthorizeRequestTokenWithURL(*args)
  203. return AuthorizeRequestTokenWithBrowser(*args)
  204. @classmethod
  205. def credential_store_factory(cls, credential_save_failed):
  206. if cls._is_sudo():
  207. # Do not try to store credentials under sudo;
  208. # it can be problematic with shared sudo access,
  209. # and we may not have access to the normal keyring provider
  210. # LP: #1862948
  211. return MemoryCredentialStore(credential_save_failed)
  212. return KeyringCredentialStore(credential_save_failed, fallback=True)
  213. @classmethod
  214. def login(
  215. cls,
  216. consumer_name,
  217. token_string,
  218. access_secret,
  219. service_root=uris.STAGING_SERVICE_ROOT,
  220. cache=None,
  221. timeout=None,
  222. proxy_info=proxy_info_from_environment,
  223. authorization_engine=None,
  224. allow_access_levels=None,
  225. max_failed_attempts=None,
  226. credential_store=None,
  227. credential_save_failed=None,
  228. version=DEFAULT_VERSION,
  229. ):
  230. """Convenience method for setting up access credentials.
  231. When all three pieces of credential information (the consumer
  232. name, the access token and the access secret) are available, this
  233. method can be used to quickly log into the service root.
  234. This method is deprecated as of launchpadlib version
  235. 1.9.0. You should use Launchpad.login_anonymously() for
  236. anonymous access, and Launchpad.login_with() for all other
  237. purposes.
  238. :param consumer_name: the application name.
  239. :type consumer_name: string
  240. :param token_string: the access token, as appropriate for the
  241. `AccessToken` constructor
  242. :type token_string: string
  243. :param access_secret: the access token's secret, as appropriate for
  244. the `AccessToken` constructor
  245. :type access_secret: string
  246. :param service_root: The URL to the root of the web service.
  247. :type service_root: string
  248. :param authorization_engine: See `Launchpad.__init__`. If you don't
  249. provide an authorization engine, a default engine will be
  250. constructed using your values for `service_root` and
  251. `credential_save_failed`.
  252. :param allow_access_levels: This argument is ignored, and only
  253. present to preserve backwards compatibility.
  254. :param max_failed_attempts: This argument is ignored, and only
  255. present to preserve backwards compatibility.
  256. :return: The web service root
  257. :rtype: `Launchpad`
  258. """
  259. cls._warn_of_deprecated_login_method("login")
  260. access_token = AccessToken(token_string, access_secret)
  261. credentials = Credentials(
  262. consumer_name=consumer_name, access_token=access_token
  263. )
  264. if authorization_engine is None:
  265. authorization_engine = cls.authorization_engine_factory(
  266. service_root, consumer_name, allow_access_levels
  267. )
  268. if credential_store is None:
  269. credential_store = cls.credential_store_factory(
  270. credential_save_failed
  271. )
  272. return cls(
  273. credentials,
  274. authorization_engine,
  275. credential_store,
  276. service_root,
  277. cache,
  278. timeout,
  279. proxy_info,
  280. version,
  281. )
  282. @classmethod
  283. def get_token_and_login(
  284. cls,
  285. consumer_name,
  286. service_root=uris.STAGING_SERVICE_ROOT,
  287. cache=None,
  288. timeout=None,
  289. proxy_info=proxy_info_from_environment,
  290. authorization_engine=None,
  291. allow_access_levels=[],
  292. max_failed_attempts=None,
  293. credential_store=None,
  294. credential_save_failed=None,
  295. version=DEFAULT_VERSION,
  296. ):
  297. """Get credentials from Launchpad and log into the service root.
  298. This method is deprecated as of launchpadlib version
  299. 1.9.0. You should use Launchpad.login_anonymously() for
  300. anonymous access and Launchpad.login_with() for all other
  301. purposes.
  302. :param consumer_name: Either a consumer name, as appropriate for
  303. the `Consumer` constructor, or a premade Consumer object.
  304. :type consumer_name: string
  305. :param service_root: The URL to the root of the web service.
  306. :type service_root: string
  307. :param authorization_engine: See `Launchpad.__init__`. If you don't
  308. provide an authorization engine, a default engine will be
  309. constructed using your values for `service_root` and
  310. `credential_save_failed`.
  311. :param allow_access_levels: This argument is ignored, and only
  312. present to preserve backwards compatibility.
  313. :return: The web service root
  314. :rtype: `Launchpad`
  315. """
  316. cls._warn_of_deprecated_login_method("get_token_and_login")
  317. return cls._authorize_token_and_login(
  318. consumer_name,
  319. service_root,
  320. cache,
  321. timeout,
  322. proxy_info,
  323. authorization_engine,
  324. allow_access_levels,
  325. credential_store,
  326. credential_save_failed,
  327. version,
  328. )
  329. @classmethod
  330. def _authorize_token_and_login(
  331. cls,
  332. consumer_name,
  333. service_root,
  334. cache,
  335. timeout,
  336. proxy_info,
  337. authorization_engine,
  338. allow_access_levels,
  339. credential_store,
  340. credential_save_failed,
  341. version,
  342. ):
  343. """Authorize a request token. Log in with the resulting access token.
  344. This is the private, non-deprecated implementation of the
  345. deprecated method get_token_and_login(). Once
  346. get_token_and_login() is removed, this code can be streamlined
  347. and moved into its other call site, login_with().
  348. """
  349. if isinstance(consumer_name, Consumer):
  350. consumer = consumer_name
  351. else:
  352. # Create a system-wide consumer. lazr.restfulclient won't
  353. # do this automatically, but launchpadlib's default is to
  354. # do a desktop-wide integration.
  355. consumer = SystemWideConsumer(consumer_name)
  356. # Create the credentials with no Consumer, then set its .consumer
  357. # property directly.
  358. credentials = Credentials(None)
  359. credentials.consumer = consumer
  360. if authorization_engine is None:
  361. authorization_engine = cls.authorization_engine_factory(
  362. service_root, consumer_name, None, allow_access_levels
  363. )
  364. if credential_store is None:
  365. credential_store = cls.credential_store_factory(
  366. credential_save_failed
  367. )
  368. else:
  369. # A credential store was passed in, so we won't be using
  370. # any provided value for credential_save_failed. But at
  371. # least make sure we weren't given a conflicting value,
  372. # since that makes the calling code look confusing.
  373. cls._assert_login_argument_consistency(
  374. "credential_save_failed",
  375. credential_save_failed,
  376. credential_store.credential_save_failed,
  377. "credential_store",
  378. )
  379. # Try to get the credentials out of the credential store.
  380. cached_credentials = credential_store.load(
  381. authorization_engine.unique_consumer_id
  382. )
  383. if cached_credentials is None:
  384. # They're not there. Acquire new credentials using the
  385. # authorization engine.
  386. credentials = authorization_engine(credentials, credential_store)
  387. else:
  388. # We acquired credentials. But, the application name
  389. # wasn't stored along with the credentials, because in a
  390. # desktop integration scenario, a single set of
  391. # credentials may be shared by many applications. We need
  392. # to set the application name for this specific instance
  393. # of the credentials.
  394. credentials = cached_credentials
  395. credentials.consumer.application_name = (
  396. authorization_engine.application_name
  397. )
  398. return cls(
  399. credentials,
  400. authorization_engine,
  401. credential_store,
  402. service_root,
  403. cache,
  404. timeout,
  405. proxy_info,
  406. version,
  407. )
  408. @classmethod
  409. def login_anonymously(
  410. cls,
  411. consumer_name,
  412. service_root=uris.STAGING_SERVICE_ROOT,
  413. launchpadlib_dir=None,
  414. timeout=None,
  415. proxy_info=proxy_info_from_environment,
  416. version=DEFAULT_VERSION,
  417. ):
  418. """Get access to Launchpad without providing any credentials."""
  419. (
  420. service_root,
  421. launchpadlib_dir,
  422. cache_path,
  423. service_root_dir,
  424. ) = cls._get_paths(service_root, launchpadlib_dir)
  425. token = AnonymousAccessToken()
  426. credentials = Credentials(consumer_name, access_token=token)
  427. return cls(
  428. credentials,
  429. None,
  430. None,
  431. service_root=service_root,
  432. cache=cache_path,
  433. timeout=timeout,
  434. proxy_info=proxy_info,
  435. version=version,
  436. )
  437. @classmethod
  438. def login_with(
  439. cls,
  440. application_name=None,
  441. service_root=uris.STAGING_SERVICE_ROOT,
  442. launchpadlib_dir=None,
  443. timeout=None,
  444. proxy_info=proxy_info_from_environment,
  445. authorization_engine=None,
  446. allow_access_levels=None,
  447. max_failed_attempts=None,
  448. credentials_file=None,
  449. version=DEFAULT_VERSION,
  450. consumer_name=None,
  451. credential_save_failed=None,
  452. credential_store=None,
  453. ):
  454. """Log in to Launchpad, possibly acquiring and storing credentials.
  455. Use this method to get a `Launchpad` object. If the end-user
  456. has no cached Launchpad credential, their browser will open
  457. and they'll be asked to log in and authorize a desktop
  458. integration. The authorized Launchpad credential will be
  459. stored securely: in the GNOME keyring, the KDE Wallet, or in
  460. an encrypted file on disk.
  461. The next time your program (or any other program run by that
  462. user on the same computer) invokes this method, the end-user
  463. will be prompted to unlock their keyring (or equivalent), and
  464. the credential will be retrieved from local storage and
  465. reused.
  466. You can customize this behavior in three ways:
  467. 1. Pass in a filename to `credentials_file`. The end-user's
  468. credential will be written to that file, and on subsequent
  469. runs read from that file. Alternatively the filename can be
  470. given in the LP_CREDENTIALS_FILE environment variable.
  471. 2. Subclass `CredentialStore` and pass in an instance of the
  472. subclass as `credential_store`. This lets you change how
  473. the end-user's credential is stored and retrieved locally.
  474. 3. Subclass `RequestTokenAuthorizationEngine` and pass in an
  475. instance of the subclass as `authorization_engine`. This
  476. lets you change change what happens when the end-user needs
  477. to authorize the Launchpad credential.
  478. :param application_name: The application name. This is *not*
  479. the OAuth consumer name. Unless a consumer_name is also
  480. provided, the OAuth consumer will be a system-wide
  481. consumer representing the end-user's computer as a whole.
  482. :type application_name: string
  483. :param service_root: The URL to the root of the web service.
  484. :type service_root: string. Can either be the full URL to a service
  485. or one of the short service names.
  486. :param launchpadlib_dir: The directory used to store cached
  487. data obtained from Launchpad. The cache is shared by all
  488. consumers, and each Launchpad service root has its own
  489. cache.
  490. :type launchpadlib_dir: string
  491. :param authorization_engine: A strategy for getting the
  492. end-user to authorize an OAuth request token, for
  493. exchanging the request token for an access token, and for
  494. storing the access token locally so that it can be
  495. reused. By default, launchpadlib will open the end-user's
  496. web browser to have them authorize the request token.
  497. :type authorization_engine: `RequestTokenAuthorizationEngine`
  498. :param allow_access_levels: The acceptable access levels for
  499. this application.
  500. This argument is used to construct the default
  501. `authorization_engine`, so if you pass in your own
  502. `authorization_engine` any value for this argument will be
  503. ignored. This argument will also be ignored unless you
  504. also specify `consumer_name`.
  505. :type allow_access_levels: list of strings
  506. :param max_failed_attempts: Ignored; only present for
  507. backwards compatibility.
  508. :param credentials_file: The path to a file in which to store
  509. this user's OAuth access token.
  510. :param version: The version of the Launchpad web service to use.
  511. :param consumer_name: The consumer name, as appropriate for
  512. the `Consumer` constructor. You probably don't want to
  513. provide this, since providing it will prevent you from
  514. taking advantage of desktop-wide integration.
  515. :type consumer_name: string
  516. :param credential_save_failed: a callback that is called upon
  517. a failure to save the credentials locally. This argument is
  518. used to construct the default `credential_store`, so if
  519. you pass in your own `credential_store` any value for
  520. this argument will be ignored.
  521. :type credential_save_failed: A callable
  522. :param credential_store: A strategy for storing an OAuth
  523. access token locally. By default, tokens are stored in the
  524. GNOME keyring (or equivalent). If `credentials_file` is
  525. provided, then tokens are stored unencrypted in that file.
  526. :type credential_store: `CredentialStore`
  527. :return: A web service root authorized as the end-user.
  528. :rtype: `Launchpad`
  529. """
  530. (
  531. service_root,
  532. launchpadlib_dir,
  533. cache_path,
  534. service_root_dir,
  535. ) = cls._get_paths(service_root, launchpadlib_dir)
  536. if (
  537. application_name is None
  538. and consumer_name is None
  539. and authorization_engine is None
  540. ):
  541. raise ValueError(
  542. "At least one of application_name, consumer_name, or "
  543. "authorization_engine must be provided."
  544. )
  545. if credentials_file is None:
  546. credentials_file = os.environ.get("LP_CREDENTIALS_FILE")
  547. if credentials_file is not None and credential_store is not None:
  548. raise ValueError(
  549. "At most one of credentials_file and credential_store "
  550. "must be provided."
  551. )
  552. if credential_store is None:
  553. if credentials_file is not None:
  554. # The end-user wants credentials stored in an
  555. # unencrypted file.
  556. credential_store = UnencryptedFileCredentialStore(
  557. credentials_file, credential_save_failed
  558. )
  559. else:
  560. credential_store = cls.credential_store_factory(
  561. credential_save_failed
  562. )
  563. else:
  564. # A credential store was passed in, so we won't be using
  565. # any provided value for credential_save_failed. But at
  566. # least make sure we weren't given a conflicting value,
  567. # since that makes the calling code look confusing.
  568. cls._assert_login_argument_consistency(
  569. "credential_save_failed",
  570. credential_save_failed,
  571. credential_store.credential_save_failed,
  572. "credential_store",
  573. )
  574. credential_store = credential_store
  575. if authorization_engine is None:
  576. authorization_engine = cls.authorization_engine_factory(
  577. service_root,
  578. application_name,
  579. consumer_name,
  580. allow_access_levels,
  581. )
  582. else:
  583. # An authorization engine was passed in, so we won't be
  584. # using any provided values for application_name,
  585. # consumer_name, or allow_access_levels. But at least make
  586. # sure we weren't given conflicting values, since that
  587. # makes the calling code look confusing.
  588. cls._assert_login_argument_consistency(
  589. "application_name",
  590. application_name,
  591. authorization_engine.application_name,
  592. )
  593. cls._assert_login_argument_consistency(
  594. "consumer_name",
  595. consumer_name,
  596. authorization_engine.consumer.key,
  597. )
  598. cls._assert_login_argument_consistency(
  599. "allow_access_levels",
  600. allow_access_levels,
  601. authorization_engine.allow_access_levels,
  602. )
  603. return cls._authorize_token_and_login(
  604. authorization_engine.consumer,
  605. service_root,
  606. cache_path,
  607. timeout,
  608. proxy_info,
  609. authorization_engine,
  610. allow_access_levels,
  611. credential_store,
  612. credential_save_failed,
  613. version,
  614. )
  615. @classmethod
  616. def _warn_of_deprecated_login_method(cls, name):
  617. warnings.warn(
  618. (
  619. "The Launchpad.%s() method is deprecated. You should use "
  620. "Launchpad.login_anonymous() for anonymous access and "
  621. "Launchpad.login_with() for all other purposes."
  622. )
  623. % name,
  624. DeprecationWarning,
  625. )
  626. @classmethod
  627. def _assert_login_argument_consistency(
  628. cls,
  629. argument_name,
  630. argument_value,
  631. object_value,
  632. object_name="authorization engine",
  633. ):
  634. """Helper to find conflicting values passed into the login methods.
  635. Many of the arguments to login_with are used to build other
  636. objects--the authorization engine or the credential store. If
  637. these objects are provided directly, many of the arguments
  638. become redundant. We'll allow redundant arguments through, but
  639. if a argument *conflicts* with the corresponding value in the
  640. provided object, we raise an error.
  641. """
  642. inconsistent_value_message = (
  643. "Inconsistent values given for %s: "
  644. "(%r passed in, versus %r in %s). "
  645. "You don't need to pass in %s if you pass in %s, "
  646. "so just omit that argument."
  647. )
  648. if argument_value is not None and argument_value != object_value:
  649. raise ValueError(
  650. inconsistent_value_message
  651. % (
  652. argument_name,
  653. argument_value,
  654. object_value,
  655. object_name,
  656. argument_name,
  657. object_name,
  658. )
  659. )
  660. @classmethod
  661. def _get_paths(cls, service_root, launchpadlib_dir=None):
  662. """Locate launchpadlib-related user paths and ensure they exist.
  663. This is a helper function used by login_with() and
  664. login_anonymously().
  665. :param service_root: The service root the user wants to
  666. connect to. This may be an alias (which will be
  667. dereferenced to a URL and returned) or a URL (which will
  668. be returned as is).
  669. :param launchpadlib_dir: The user's base launchpadlib
  670. directory, if known. This may be modified, expanded, or
  671. determined from the environment if missing. A definitive
  672. value will be returned.
  673. :return: A 4-tuple:
  674. (service_root_uri, launchpadlib_dir, cache_dir, service_root_dir)
  675. """
  676. if launchpadlib_dir is None:
  677. launchpadlib_dir = os.path.join("~", ".launchpadlib")
  678. launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
  679. if launchpadlib_dir[:1] == "~":
  680. raise ValueError(
  681. "Must set $HOME or pass 'launchpadlib_dir' to "
  682. "indicate location to store cached data"
  683. )
  684. try:
  685. os.makedirs(launchpadlib_dir, 0o700)
  686. except OSError as err:
  687. if err.errno != errno.EEXIST:
  688. raise
  689. os.chmod(launchpadlib_dir, 0o700)
  690. # Determine the real service root.
  691. service_root = uris.lookup_service_root(service_root)
  692. # Each service root has its own cache and credential dirs.
  693. scheme, host_name, path, query, fragment = urlsplit(service_root)
  694. service_root_dir = os.path.join(launchpadlib_dir, host_name)
  695. cache_path = os.path.join(service_root_dir, "cache")
  696. try:
  697. os.makedirs(cache_path, 0o700)
  698. except OSError as err:
  699. if err.errno != errno.EEXIST:
  700. raise
  701. return (service_root, launchpadlib_dir, cache_path, service_root_dir)