launchpadlib-1.10.2/ 0000755 0000000 0000000 00000000000 12074266543 012730 5 ustar root root launchpadlib-1.10.2/PKG-INFO 0000644 0000000 0000000 00000026750 11775571157 014046 0 ustar root root Metadata-Version: 1.1
Name: launchpadlib
Version: 1.10.2
Summary: Script Launchpad through its web services interfaces. Officially supported.
Home-page: https://help.launchpad.net/API/launchpadlib
Author: LAZR Developers
Author-email: lazr-developers@lists.launchpad.net
License: LGPL v3
Download-URL: https://launchpad.net/launchpadlib/+download
Description: ..
This file is part of launchpadlib.
launchpadlib is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 of the License.
launchpadlib is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with launchpadlib. If not, see .
launchpadlib
************
See https://help.launchpad.net/API/launchpadlib .
=====================
NEWS for launchpadlib
=====================
1.10.2 (2012-07-05)
===================
- Typo in the doctest fix, discovered when trying to integrate with launchpad
itself. [bug=1020667]
1.10.1 (2012-07-04)
===================
- Fix a doctest in introduction.txt so that the test suite runs with
python-2.7 (note the doctests only run when running integrated with
launchpad's test suite itself). [bug=1020667]
1.10.0 (2012-06-19)
===================
- Add environment variable, LP_DISABLE_SSL_CERTIFICATE_VALIDATION, to
disable SSL certificate checks. Most useful when testing against
development servers.
1.9.12 (2011-12-05)
===================
- Move keyring base64 encoding to KeyringCredential and be more
defensive about decoding. [bug=900307]
1.9.11 (2011-11-21)
===================
- 1.9.10 was a bad release due to incomplete NEWS entries.
- Add fake Launchpad web service for unit test.
- Improve HACKING documentation.
- Improve launchpadlib directory discovery on Windows.
- Added script to delete spurious bugtasks or split a bugtask from a bug.
- Properly handle Unicode passwords if returned by the keyring.
- Base 64 encode serialized credentials before putting in keyring/wallet.
1.9.10 (2011-11-21)
===================
- Base 64 encode serialized credentials before putting in keyring/wallet.
1.9.9 (2011-07-27)
==================
- Fix a failing test for lazr.restfulclient 0.12.0.
1.9.8 (2011-02-28)
==================
- Detect the error Launchpad sends when it doesn't recognize an access
token, and get a new token.
1.9.7 (2011-02-15)
==================
- Slightly tweaked the behavior of EDGE_SERVICE_ROOT, and improved tests.
1.9.6 (2011-02-14)
==================
- Added EDGE_SERVICE_ROOT and the 'edge' alias back, though they both
operate on production behind the scenes. Using the 'edge' alias will
cause a deprecation warning.
1.9.5 (2011-02-08)
==================
- Fixed a bug that prevented the deprecated get_token_and_login code
from working, and that required that users of get_token_and_login
get a new token on every usage.
1.9.4 (2011-01-18)
==================
- Removed references to the 'edge' service root, which is being phased out.
- Fixed a minor bug in the upload_release_tarball contrib script which
was causing tarballs to be uploaded with the wrong media type.
- The XSLT stylesheet for converting the Launchpad WADL into HTML
documentation has been moved back into Launchpad.
1.9.3 (2011-01-10)
==================
- The keyring package import is now delayed until the keyring needs to be
accessed. This reduces launchapdlib users' exposure to unintended side
effects of importing keyring (KWallet authorization dialogs and the
registration of a SIGCHLD handler).
1.9.2 (2011-01-07)
==================
- Added a missing import.
1.9.1 (2011-01-06)
==================
- Corrected a test failure.
1.9.0 (2011-01-05)
==================
- When an authorization token expires or becomes invalid, attempt to
acquire a new one, even in the middle of a session, rather than
crashing.
- The HTML generated by wadl-to-refhtml.xsl now validates.
- Most of the helper login methods have been deprecated. There are now
only two helper methods:
* Launchpad.login_anonymously, for anonymous credential-free access.
* Launchpad.login_with, for programs that need a credential.
1.8.0 (2010-11-15)
==================
- Store authorization tokens in the Gnome keyring or KDE wallet, when
available. The credentials_file parameter of Launchpad.login_with() is now
ignored.
- By default, Launchpad.login_with() now asks Launchpad for
desktop-wide integration. This removes the need for each individual
application to get its own OAuth token.
1.7.0 (2010-09-23)
==================
- Removed "fake Launchpad browser" code that didn't work and was
misleading developers.
- Added support for http://qastaging.launchpad.net by adding
astaging to the uris.
1.6.5 (2010-08-23)
==================
- Make launchpadlib compatible with the latest lazr.restfulclient.
1.6.4 (2010-08-18)
==================
- Test fixes.
1.6.3 (2010-08-12)
==================
- Instead of making the end-user hit Enter after authorizing an
application to access their Launchpad account, launchpadlib will
automatically poll Launchpad until the user makes a decision.
- launchpadlib now raises a more helpful exception when the end-user
explicitly denies access to a launchpadlib application.
- Improved the XSLT stylesheet to reflect Launchpad's more complex
top-level structure. [bug=286941]
- Test fixes. [bug=488448,616055]
1.6.2 (2010-06-21)
==================
- Extended the optimization from version 1.6.1 to apply to Launchpad's
top-level collection of people.
1.6.1 (2010-06-16)
==================
- Added an optimization that lets launchpadlib avoid making an HTTP
request in some situations.
1.6.0 (2010-04-07)
==================
- Fixed a test to work against the latest version of Launchpad.
1.5.8 (2010-03-25)
==================
- Use version 1.0 of the Launchpad web service by default.
1.5.7 (2010-03-16)
==================
- Send a Referer header whenever making requests to the Launchpad
website (as opposed to the web service) to avoid falling afoul of
new cross-site-request-forgery countermeasures.
1.5.6 (2010-03-04)
==================
- Fixed a minor bug when using login_with() to access a version of the
Launchpad web service other than the default.
- Added a check to catch old client code that would cause newer
versions of launchpadlib to make nonsensical requests to
https://api.launchpad.dev/beta/beta/, and raise a helpful exception
telling the developer how to fix it.
1.5.5
=====
- Added the ability to access different versions of the Launchpad web
service.
1.5.4 (2009-12-17)
==================
- Made it easy to get anonymous access to a Launchpad instance.
- Made it easy to plug in different clients that take the user's
Launchpad login and password for purposes of authorizing a request
token. The most secure technique is still the default: to open the
user's web browser to the appropriate Launchpad page.
- Introduced a command-line script bin/launchpad-credentials-console,
which takes the user's Launchpad login and password, and authorizes
a request token on their behalf.
- Introduced a command-line script bin/launchpad-request-token, which
creates a request token on any Launchpad installation and dumps the
JSON description of that token to standard output.
- Shorthand service names like 'edge' should now be respected
everywhere in launchpadlib.
1.5.3 (2009-10-22)
==================
- Moved some more code from launchpadlib into the more generic
lazr.restfulclient.
1.5.2 (2009-10-01)
==================
- Added a number of new sample scripts from elsewhere.
- Added a reference to the production Launchpad instance.
- Made it easier to specify a Launchpad instance to run against.
1.5.1 (2009-07-16)
==================
- Added a sample script for uploading a release tarball to Launchpad.
1.5.0 (2009-07-09)
==================
- Most of launchpadlib's code has been moved to the generic
lazr.restfulclient library. launchpadlib now contains only code
specific to Launchpad. There should be no changes in functionality.
- Moved bootstrap.py into the top-level directory. Having it in a
subdirectory with a top-level symlink was breaking installation on
Windows.
- The notice to the end-user (that we're opening their web
browser) is now better formatted.
1.0.1 (2009-05-30)
==================
- Correct tests for new launchpad cache behavior in librarian
- Remove build dependency on setuptools_bzr because it was causing bzr to be
downloaded during installation of the package, which was unnecessary and
annoying.
1.0 (2009-03-24)
================
- Initial release on PyPI
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
launchpadlib-1.10.2/COPYING.txt 0000644 0000000 0000000 00000016727 11775055270 014616 0 ustar root root GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
launchpadlib-1.10.2/ez_setup.py 0000644 0000000 0000000 00000022467 11775055270 015153 0 ustar root root #!python
"""Bootstrap setuptools installation
If you want to use setuptools in your package's setup.py, just include this
file in the same directory with it, and add this to the top of your setup.py::
from ez_setup import use_setuptools
use_setuptools()
If you want to require a specific version of setuptools, set a download
mirror, or use an alternate download directory, you can do so by supplying
the appropriate options to ``use_setuptools()``.
This file can also be run as a script to install or upgrade setuptools.
"""
import sys
DEFAULT_VERSION = "0.6c11"
DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
md5_data = {
'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
}
import sys, os
def _validate_md5(egg_name, data):
if egg_name in md5_data:
from md5 import md5
digest = md5(data).hexdigest()
if digest != md5_data[egg_name]:
print >>sys.stderr, (
"md5 validation of %s failed! (Possible download problem?)"
% egg_name
)
sys.exit(2)
return data
def use_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
download_delay=15, min_version=None
):
"""Automatically find/download setuptools and make it available on sys.path
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end with
a '/'). `to_dir` is the directory where setuptools will be downloaded, if
it is not already available. If `download_delay` is specified, it should
be the number of seconds that will be paused before initiating a download,
should one be required. If an older version of setuptools is installed,
this routine will print a message to ``sys.stderr`` and raise SystemExit in
an attempt to abort the calling script.
"""
# Work around a hack in the ez_setup.py file from simplejson==1.7.3.
if min_version:
version = min_version
was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
def do_download():
egg = download_setuptools(version, download_base, to_dir, download_delay)
sys.path.insert(0, egg)
import setuptools; setuptools.bootstrap_install_from = egg
try:
import pkg_resources
except ImportError:
return do_download()
try:
pkg_resources.require("setuptools>="+version); return
except pkg_resources.VersionConflict, e:
if was_imported:
print >>sys.stderr, (
"The required version of setuptools (>=%s) is not available, and\n"
"can't be installed while this script is running. Please install\n"
" a more recent version first, using 'easy_install -U setuptools'."
"\n\n(Currently using %r)"
) % (version, e.args[0])
sys.exit(2)
else:
del pkg_resources, sys.modules['pkg_resources'] # reload ok
return do_download()
except pkg_resources.DistributionNotFound:
return do_download()
def download_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
delay = 15
):
"""Download setuptools from a specified location and return its filename
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end
with a '/'). `to_dir` is the directory where the egg will be downloaded.
`delay` is the number of seconds to pause before an actual download attempt.
"""
import urllib2, shutil
egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
url = download_base + egg_name
saveto = os.path.join(to_dir, egg_name)
src = dst = None
if not os.path.exists(saveto): # Avoid repeated downloads
try:
from distutils import log
if delay:
log.warn("""
---------------------------------------------------------------------------
This script requires setuptools version %s to run (even to display
help). I will attempt to download it for you (from
%s), but
you may need to enable firewall access for this script first.
I will start the download in %d seconds.
(Note: if this machine does not have network access, please obtain the file
%s
and place it in this directory before rerunning this script.)
---------------------------------------------------------------------------""",
version, download_base, delay, url
); from time import sleep; sleep(delay)
log.warn("Downloading %s", url)
src = urllib2.urlopen(url)
# Read/write all in one block, so we don't create a corrupt file
# if the download is interrupted.
data = _validate_md5(egg_name, src.read())
dst = open(saveto,"wb"); dst.write(data)
finally:
if src: src.close()
if dst: dst.close()
return os.path.realpath(saveto)
def main(argv, version=DEFAULT_VERSION):
"""Install or upgrade setuptools and EasyInstall"""
try:
import setuptools
except ImportError:
egg = None
try:
egg = download_setuptools(version, delay=0)
sys.path.insert(0,egg)
from setuptools.command.easy_install import main
return main(list(argv)+[egg]) # we're done here
finally:
if egg and os.path.exists(egg):
os.unlink(egg)
else:
if setuptools.__version__ == '0.0.1':
print >>sys.stderr, (
"You have an obsolete version of setuptools installed. Please\n"
"remove it from your system entirely before rerunning this script."
)
sys.exit(2)
req = "setuptools>="+version
import pkg_resources
try:
pkg_resources.require(req)
except pkg_resources.VersionConflict:
try:
from setuptools.command.easy_install import main
except ImportError:
from easy_install import main
main(list(argv)+[download_setuptools(delay=0)])
sys.exit(0) # try to force an exit
else:
if argv:
from setuptools.command.easy_install import main
main(argv)
else:
print "Setuptools version",version,"or greater has been installed."
print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
def update_md5(filenames):
"""Update our built-in md5 registry"""
import re
from md5 import md5
for name in filenames:
base = os.path.basename(name)
f = open(name,'rb')
md5_data[base] = md5(f.read()).hexdigest()
f.close()
data = [" %r: %r,\n" % it for it in md5_data.items()]
data.sort()
repl = "".join(data)
import inspect
srcfile = inspect.getsourcefile(sys.modules[__name__])
f = open(srcfile, 'rb'); src = f.read(); f.close()
match = re.search("\nmd5_data = {\n([^}]+)}", src)
if not match:
print >>sys.stderr, "Internal error!"
sys.exit(2)
src = src[:match.start(1)] + repl + src[match.end(1):]
f = open(srcfile,'w')
f.write(src)
f.close()
if __name__=='__main__':
if len(sys.argv)>2 and sys.argv[1]=='--md5update':
update_md5(sys.argv[2:])
else:
main(sys.argv[1:])
launchpadlib-1.10.2/setup.py 0000755 0000000 0000000 00000004754 11775055270 014457 0 ustar root root #!/usr/bin/env python
# Copyright 2008-2009 Canonical Ltd.
#
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Setup for the launchpadlib library."""
import ez_setup
ez_setup.use_setuptools()
import sys
from setuptools import setup, find_packages
# generic helpers primarily for the long_description
def generate(*docname_or_string):
res = []
for value in docname_or_string:
if value.endswith('.txt'):
f = open(value)
value = f.read()
f.close()
res.append(value)
if not value.endswith('\n'):
res.append('')
return '\n'.join(res)
# end generic helpers
sys.path.insert(0, 'src')
from launchpadlib import __version__
setup(
name='launchpadlib',
version=__version__,
packages=find_packages('src'),
package_dir={'':'src'},
include_package_data=True,
zip_safe=False,
author='The Launchpad developers',
author_email='launchpadlib@lists.launchpad.net',
maintainer='LAZR Developers',
maintainer_email='lazr-developers@lists.launchpad.net',
download_url= 'https://launchpad.net/launchpadlib/+download',
description=open('README.txt').readline().strip(),
long_description=generate(
'src/launchpadlib/README.txt',
'src/launchpadlib/NEWS.txt'),
license='LGPL v3',
install_requires=[
'httplib2',
'keyring',
'lazr.restfulclient>=0.9.19',
'lazr.uri',
'oauth',
'setuptools',
'simplejson',
'testresources',
'wadllib',
],
url='https://help.launchpad.net/API/launchpadlib',
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Operating System :: OS Independent",
"Programming Language :: Python"],
test_suite='launchpadlib.tests',
)
launchpadlib-1.10.2/README.txt 0000644 0000000 0000000 00000003704 11775055270 014432 0 ustar root root Script Launchpad through its web services interfaces. Officially supported.
..
Copyright (C) 2008-2009 Canonical Ltd.
This file is part of launchpadlib.
launchpadlib is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 of the License.
launchpadlib is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with launchpadlib. If not, see .
Overview
========
launchpadlib is a standalone Python library for scripting Launchpad through
its web services interface. It is the officially supported bindings to the
Launchpad web service, but there may be third party bindings that provide
scriptability for other languages.
Launchpad (http://launchpad.net) is a a free software hosting and development
website, making it easy to collaborate across multiple projects. For
information on Launchpad itself, see
https://help.launchpad.net
More information on the Launchpad web service, such as user guides and
reference documentation, are available at
https://help.launchpad.net/API
Client documentation for launchpadlib is available at
https://help.launchpad.net/API/launchpadlib
Please submit bug reports to
https://bugs.launchpad.net/launchpadlib
Credential storage
==================
After authorizing an application to access Launchpad on a user's behalf,
launchpadlib will attempt to store those credentials in the most
system-appropriate way. That is, on Gnome it will store them in the Gnome
keyring, on KDE it will store them in the KDE wallet and if neither of those
is available it will store them on disk.
launchpadlib-1.10.2/HACKING.txt 0000644 0000000 0000000 00000003271 11775055270 014540 0 ustar root root ..
This file is part of lazr.launchpadlib.
lazr.launchpadlib is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 of the License.
lazr.launchpadlib is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with lazr.launchpadlib. If not, see .
This project uses zc.buildout for development.
============
Introduction
============
These are guidelines for hacking on the lazr.launchpadlib project. But first,
please see the common hacking guidelines at:
https://dev.launchpad.net/HackingLazrLibraries
These guidelines will tell you how to run the tests, use in-development
versions of libraries, land your branches and get them released.
Getting help
------------
If you find bugs in this package, you can report them here:
https://launchpad.net/launchpadlib
If you want to discuss this package, join the team and mailing list here:
https://launchpad.net/~lazr-developers
or send a message to:
lazr-developers@lists.launchpad.net
========
Building
========
As mentioned above, this project uses zc.buildout, which requires a
few steps to build some necessary files.
The following two steps will create some necessary files, including a
bin directory.
% python bootstrap.py
% bin/buildout
Once built you can run all of the tests with:
% bin/test
launchpadlib-1.10.2/src/ 0000755 0000000 0000000 00000000000 11775571156 013525 5 ustar root root launchpadlib-1.10.2/src/launchpadlib.egg-info/ 0000755 0000000 0000000 00000000000 11775571157 017646 5 ustar root root launchpadlib-1.10.2/src/launchpadlib.egg-info/SOURCES.txt 0000644 0000000 0000000 00000037246 11775571156 021545 0 ustar root root COPYING.txt
HACKING.txt
README.txt
ez_setup.py
setup.py
eggs/docutils-0.9.1-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/docutils-0.9.1-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/docutils-0.9.1-py2.7.egg/EGG-INFO/top_level.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/README.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isoamsa.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isoamsb.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isoamsc.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isoamsn.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isoamso.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isoamsr.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isobox.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isocyr1.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isocyr2.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isodia.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isogrk1.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isogrk2.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isogrk3.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isogrk4-wide.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isogrk4.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isolat1.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isolat2.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isomfrk-wide.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isomfrk.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isomopf-wide.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isomopf.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isomscr-wide.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isomscr.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isonum.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isopub.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/isotech.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/mmlalias.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/mmlextra-wide.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/mmlextra.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/s5defs.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/xhtml1-lat1.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/xhtml1-special.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/parsers/rst/include/xhtml1-symbol.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/writers/html4css1/template.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/writers/pep_html/template.txt
eggs/docutils-0.9.1-py2.7.egg/docutils/writers/s5_html/themes/README.txt
eggs/httplib2-0.7.4-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/httplib2-0.7.4-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/httplib2-0.7.4-py2.7.egg/EGG-INFO/top_level.txt
eggs/httplib2-0.7.4-py2.7.egg/httplib2/cacerts.txt
eggs/keyring-0.9.2-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/keyring-0.9.2-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/keyring-0.9.2-py2.7.egg/EGG-INFO/requires.txt
eggs/keyring-0.9.2-py2.7.egg/EGG-INFO/top_level.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/EGG-INFO/requires.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/EGG-INFO/top_level.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/lazr/authentication/NEWS.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/lazr/authentication/README.txt
eggs/lazr.authentication-0.1.2-py2.7.egg/lazr/authentication/version.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/EGG-INFO/requires.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/EGG-INFO/top_level.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/NEWS.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/README.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/version.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/authorizer.standalone.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/caching.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/collections.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/entries.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/hosted-files.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/operations.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/retry.standalone.txt
eggs/lazr.restfulclient-0.13.0-py2.7.egg/lazr/restfulclient/docs/toplevel.txt
eggs/lazr.uri-1.0.3-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/lazr.uri-1.0.3-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/lazr.uri-1.0.3-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/lazr.uri-1.0.3-py2.7.egg/EGG-INFO/requires.txt
eggs/lazr.uri-1.0.3-py2.7.egg/EGG-INFO/top_level.txt
eggs/lazr.uri-1.0.3-py2.7.egg/lazr/uri/NEWS.txt
eggs/lazr.uri-1.0.3-py2.7.egg/lazr/uri/README.txt
eggs/lazr.uri-1.0.3-py2.7.egg/lazr/uri/version.txt
eggs/oauth-1.0.1-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/oauth-1.0.1-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/oauth-1.0.1-py2.7.egg/EGG-INFO/top_level.txt
eggs/setuptools-0.6c12dev_r88846-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/setuptools-0.6c12dev_r88846-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/setuptools-0.6c12dev_r88846-py2.7.egg/EGG-INFO/entry_points.txt
eggs/setuptools-0.6c12dev_r88846-py2.7.egg/EGG-INFO/top_level.txt
eggs/simplejson-2.6.0-py2.7-linux-i686.egg/EGG-INFO/SOURCES.txt
eggs/simplejson-2.6.0-py2.7-linux-i686.egg/EGG-INFO/dependency_links.txt
eggs/simplejson-2.6.0-py2.7-linux-i686.egg/EGG-INFO/native_libs.txt
eggs/simplejson-2.6.0-py2.7-linux-i686.egg/EGG-INFO/top_level.txt
eggs/testresources-0.2.5-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/testresources-0.2.5-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/testresources-0.2.5-py2.7.egg/EGG-INFO/top_level.txt
eggs/wadllib-1.3.1-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/wadllib-1.3.1-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/wadllib-1.3.1-py2.7.egg/EGG-INFO/requires.txt
eggs/wadllib-1.3.1-py2.7.egg/EGG-INFO/top_level.txt
eggs/wadllib-1.3.1-py2.7.egg/wadllib/NEWS.txt
eggs/wadllib-1.3.1-py2.7.egg/wadllib/README.txt
eggs/wadllib-1.3.1-py2.7.egg/wadllib/version.txt
eggs/wsgi_intercept-0.5.1-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/wsgi_intercept-0.5.1-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/wsgi_intercept-0.5.1-py2.7.egg/EGG-INFO/top_level.txt
eggs/z3c.recipe.scripts-1.0.1-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/z3c.recipe.scripts-1.0.1-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/z3c.recipe.scripts-1.0.1-py2.7.egg/EGG-INFO/entry_points.txt
eggs/z3c.recipe.scripts-1.0.1-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/z3c.recipe.scripts-1.0.1-py2.7.egg/EGG-INFO/requires.txt
eggs/z3c.recipe.scripts-1.0.1-py2.7.egg/EGG-INFO/top_level.txt
eggs/z3c.recipe.scripts-1.0.1-py2.7.egg/z3c/recipe/scripts/README.txt
eggs/z3c.recipe.tag-0.4.1-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/z3c.recipe.tag-0.4.1-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/z3c.recipe.tag-0.4.1-py2.7.egg/EGG-INFO/entry_points.txt
eggs/z3c.recipe.tag-0.4.1-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/z3c.recipe.tag-0.4.1-py2.7.egg/EGG-INFO/requires.txt
eggs/z3c.recipe.tag-0.4.1-py2.7.egg/EGG-INFO/top_level.txt
eggs/zc.buildout-1.5.2-py2.7.egg/README.txt
eggs/zc.buildout-1.5.2-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/zc.buildout-1.5.2-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/zc.buildout-1.5.2-py2.7.egg/EGG-INFO/entry_points.txt
eggs/zc.buildout-1.5.2-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/zc.buildout-1.5.2-py2.7.egg/EGG-INFO/requires.txt
eggs/zc.buildout-1.5.2-py2.7.egg/EGG-INFO/top_level.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/allowhosts.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/bootstrap.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/buildout.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/debugging.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/dependencylinks.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/distribute.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/download.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/downloadcache.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/easy_install.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/extends-cache.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/repeatable.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/runsetup.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/setup.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/testing.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/testing_bugfix.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/unzip.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/update.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/upgrading_distribute.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/virtualenv.txt
eggs/zc.buildout-1.5.2-py2.7.egg/zc/buildout/windows.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/EGG-INFO/entry_points.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/EGG-INFO/requires.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/EGG-INFO/top_level.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/zc/recipe/egg/README.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/zc/recipe/egg/api.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/zc/recipe/egg/custom.txt
eggs/zc.recipe.egg-1.3.2-py2.7.egg/zc/recipe/egg/selecting-python.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/EGG-INFO/entry_points.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/EGG-INFO/requires.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/EGG-INFO/top_level.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/zc/recipe/testrunner/README.txt
eggs/zc.recipe.testrunner-1.4.0-py2.7.egg/zc/recipe/testrunner/bugfixes.txt
eggs/zope.exceptions-4.0.0.1-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/zope.exceptions-4.0.0.1-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/zope.exceptions-4.0.0.1-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/zope.exceptions-4.0.0.1-py2.7.egg/EGG-INFO/requires.txt
eggs/zope.exceptions-4.0.0.1-py2.7.egg/EGG-INFO/top_level.txt
eggs/zope.interface-4.0.1-py2.7-linux-i686.egg/EGG-INFO/SOURCES.txt
eggs/zope.interface-4.0.1-py2.7-linux-i686.egg/EGG-INFO/dependency_links.txt
eggs/zope.interface-4.0.1-py2.7-linux-i686.egg/EGG-INFO/namespace_packages.txt
eggs/zope.interface-4.0.1-py2.7-linux-i686.egg/EGG-INFO/native_libs.txt
eggs/zope.interface-4.0.1-py2.7-linux-i686.egg/EGG-INFO/requires.txt
eggs/zope.interface-4.0.1-py2.7-linux-i686.egg/EGG-INFO/top_level.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/EGG-INFO/SOURCES.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/EGG-INFO/dependency_links.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/EGG-INFO/entry_points.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/EGG-INFO/namespace_packages.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/EGG-INFO/requires.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/EGG-INFO/top_level.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-arguments.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-colors.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-coverage-win32.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-coverage.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-debugging.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-discovery.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-edge-cases.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-errors.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-gc.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-knit.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-layers-api.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-layers-buff.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-layers-ntd.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-layers.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-leaks-err.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-leaks.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-profiling-cprofiler.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-profiling.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-progress.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-repeat.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-shuffle.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-simple.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-subunit-err.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-subunit-leaks.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-subunit.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-tb-format.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-test-selection.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-unexpected-success.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-verbose.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-wo-source.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/README.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sampletests.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sampletestsl.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sample2/e.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sample3/post_mortem5.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sample3/post_mortem6.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sample3/post_mortem_failure.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sample3/set_trace5.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/sample3/set_trace6.txt
eggs/zope.testrunner-4.0.4-py2.7.egg/zope/testrunner/testrunner-ex/usecompiled/README.txt
src/launchpadlib/NEWS.txt
src/launchpadlib/README.txt
src/launchpadlib/__init__.py
src/launchpadlib/apps.py
src/launchpadlib/credentials.py
src/launchpadlib/errors.py
src/launchpadlib/launchpad.py
src/launchpadlib/uris.py
src/launchpadlib.egg-info/PKG-INFO
src/launchpadlib.egg-info/SOURCES.txt
src/launchpadlib.egg-info/dependency_links.txt
src/launchpadlib.egg-info/not-zip-safe
src/launchpadlib.egg-info/requires.txt
src/launchpadlib.egg-info/top_level.txt
src/launchpadlib/docs/command-line.txt
src/launchpadlib/docs/hosted-files.txt
src/launchpadlib/docs/introduction.txt
src/launchpadlib/docs/operations.txt
src/launchpadlib/docs/people.txt
src/launchpadlib/docs/toplevel.txt
src/launchpadlib/docs/files/mugshot.png
src/launchpadlib/testing/__init__.py
src/launchpadlib/testing/helpers.py
src/launchpadlib/testing/launchpad.py
src/launchpadlib/testing/resources.py
src/launchpadlib/testing/tests/__init__.py
src/launchpadlib/testing/tests/test_launchpad.py
src/launchpadlib/tests/__init__.py
src/launchpadlib/tests/test_credential_store.py
src/launchpadlib/tests/test_http.py
src/launchpadlib/tests/test_launchpad.py launchpadlib-1.10.2/src/launchpadlib.egg-info/PKG-INFO 0000644 0000000 0000000 00000026750 11775571156 020754 0 ustar root root Metadata-Version: 1.1
Name: launchpadlib
Version: 1.10.2
Summary: Script Launchpad through its web services interfaces. Officially supported.
Home-page: https://help.launchpad.net/API/launchpadlib
Author: LAZR Developers
Author-email: lazr-developers@lists.launchpad.net
License: LGPL v3
Download-URL: https://launchpad.net/launchpadlib/+download
Description: ..
This file is part of launchpadlib.
launchpadlib is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 of the License.
launchpadlib is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with launchpadlib. If not, see .
launchpadlib
************
See https://help.launchpad.net/API/launchpadlib .
=====================
NEWS for launchpadlib
=====================
1.10.2 (2012-07-05)
===================
- Typo in the doctest fix, discovered when trying to integrate with launchpad
itself. [bug=1020667]
1.10.1 (2012-07-04)
===================
- Fix a doctest in introduction.txt so that the test suite runs with
python-2.7 (note the doctests only run when running integrated with
launchpad's test suite itself). [bug=1020667]
1.10.0 (2012-06-19)
===================
- Add environment variable, LP_DISABLE_SSL_CERTIFICATE_VALIDATION, to
disable SSL certificate checks. Most useful when testing against
development servers.
1.9.12 (2011-12-05)
===================
- Move keyring base64 encoding to KeyringCredential and be more
defensive about decoding. [bug=900307]
1.9.11 (2011-11-21)
===================
- 1.9.10 was a bad release due to incomplete NEWS entries.
- Add fake Launchpad web service for unit test.
- Improve HACKING documentation.
- Improve launchpadlib directory discovery on Windows.
- Added script to delete spurious bugtasks or split a bugtask from a bug.
- Properly handle Unicode passwords if returned by the keyring.
- Base 64 encode serialized credentials before putting in keyring/wallet.
1.9.10 (2011-11-21)
===================
- Base 64 encode serialized credentials before putting in keyring/wallet.
1.9.9 (2011-07-27)
==================
- Fix a failing test for lazr.restfulclient 0.12.0.
1.9.8 (2011-02-28)
==================
- Detect the error Launchpad sends when it doesn't recognize an access
token, and get a new token.
1.9.7 (2011-02-15)
==================
- Slightly tweaked the behavior of EDGE_SERVICE_ROOT, and improved tests.
1.9.6 (2011-02-14)
==================
- Added EDGE_SERVICE_ROOT and the 'edge' alias back, though they both
operate on production behind the scenes. Using the 'edge' alias will
cause a deprecation warning.
1.9.5 (2011-02-08)
==================
- Fixed a bug that prevented the deprecated get_token_and_login code
from working, and that required that users of get_token_and_login
get a new token on every usage.
1.9.4 (2011-01-18)
==================
- Removed references to the 'edge' service root, which is being phased out.
- Fixed a minor bug in the upload_release_tarball contrib script which
was causing tarballs to be uploaded with the wrong media type.
- The XSLT stylesheet for converting the Launchpad WADL into HTML
documentation has been moved back into Launchpad.
1.9.3 (2011-01-10)
==================
- The keyring package import is now delayed until the keyring needs to be
accessed. This reduces launchapdlib users' exposure to unintended side
effects of importing keyring (KWallet authorization dialogs and the
registration of a SIGCHLD handler).
1.9.2 (2011-01-07)
==================
- Added a missing import.
1.9.1 (2011-01-06)
==================
- Corrected a test failure.
1.9.0 (2011-01-05)
==================
- When an authorization token expires or becomes invalid, attempt to
acquire a new one, even in the middle of a session, rather than
crashing.
- The HTML generated by wadl-to-refhtml.xsl now validates.
- Most of the helper login methods have been deprecated. There are now
only two helper methods:
* Launchpad.login_anonymously, for anonymous credential-free access.
* Launchpad.login_with, for programs that need a credential.
1.8.0 (2010-11-15)
==================
- Store authorization tokens in the Gnome keyring or KDE wallet, when
available. The credentials_file parameter of Launchpad.login_with() is now
ignored.
- By default, Launchpad.login_with() now asks Launchpad for
desktop-wide integration. This removes the need for each individual
application to get its own OAuth token.
1.7.0 (2010-09-23)
==================
- Removed "fake Launchpad browser" code that didn't work and was
misleading developers.
- Added support for http://qastaging.launchpad.net by adding
astaging to the uris.
1.6.5 (2010-08-23)
==================
- Make launchpadlib compatible with the latest lazr.restfulclient.
1.6.4 (2010-08-18)
==================
- Test fixes.
1.6.3 (2010-08-12)
==================
- Instead of making the end-user hit Enter after authorizing an
application to access their Launchpad account, launchpadlib will
automatically poll Launchpad until the user makes a decision.
- launchpadlib now raises a more helpful exception when the end-user
explicitly denies access to a launchpadlib application.
- Improved the XSLT stylesheet to reflect Launchpad's more complex
top-level structure. [bug=286941]
- Test fixes. [bug=488448,616055]
1.6.2 (2010-06-21)
==================
- Extended the optimization from version 1.6.1 to apply to Launchpad's
top-level collection of people.
1.6.1 (2010-06-16)
==================
- Added an optimization that lets launchpadlib avoid making an HTTP
request in some situations.
1.6.0 (2010-04-07)
==================
- Fixed a test to work against the latest version of Launchpad.
1.5.8 (2010-03-25)
==================
- Use version 1.0 of the Launchpad web service by default.
1.5.7 (2010-03-16)
==================
- Send a Referer header whenever making requests to the Launchpad
website (as opposed to the web service) to avoid falling afoul of
new cross-site-request-forgery countermeasures.
1.5.6 (2010-03-04)
==================
- Fixed a minor bug when using login_with() to access a version of the
Launchpad web service other than the default.
- Added a check to catch old client code that would cause newer
versions of launchpadlib to make nonsensical requests to
https://api.launchpad.dev/beta/beta/, and raise a helpful exception
telling the developer how to fix it.
1.5.5
=====
- Added the ability to access different versions of the Launchpad web
service.
1.5.4 (2009-12-17)
==================
- Made it easy to get anonymous access to a Launchpad instance.
- Made it easy to plug in different clients that take the user's
Launchpad login and password for purposes of authorizing a request
token. The most secure technique is still the default: to open the
user's web browser to the appropriate Launchpad page.
- Introduced a command-line script bin/launchpad-credentials-console,
which takes the user's Launchpad login and password, and authorizes
a request token on their behalf.
- Introduced a command-line script bin/launchpad-request-token, which
creates a request token on any Launchpad installation and dumps the
JSON description of that token to standard output.
- Shorthand service names like 'edge' should now be respected
everywhere in launchpadlib.
1.5.3 (2009-10-22)
==================
- Moved some more code from launchpadlib into the more generic
lazr.restfulclient.
1.5.2 (2009-10-01)
==================
- Added a number of new sample scripts from elsewhere.
- Added a reference to the production Launchpad instance.
- Made it easier to specify a Launchpad instance to run against.
1.5.1 (2009-07-16)
==================
- Added a sample script for uploading a release tarball to Launchpad.
1.5.0 (2009-07-09)
==================
- Most of launchpadlib's code has been moved to the generic
lazr.restfulclient library. launchpadlib now contains only code
specific to Launchpad. There should be no changes in functionality.
- Moved bootstrap.py into the top-level directory. Having it in a
subdirectory with a top-level symlink was breaking installation on
Windows.
- The notice to the end-user (that we're opening their web
browser) is now better formatted.
1.0.1 (2009-05-30)
==================
- Correct tests for new launchpad cache behavior in librarian
- Remove build dependency on setuptools_bzr because it was causing bzr to be
downloaded during installation of the package, which was unnecessary and
annoying.
1.0 (2009-03-24)
================
- Initial release on PyPI
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
launchpadlib-1.10.2/src/launchpadlib.egg-info/dependency_links.txt 0000644 0000000 0000000 00000000001 11775571156 023713 0 ustar root root
launchpadlib-1.10.2/src/launchpadlib.egg-info/top_level.txt 0000644 0000000 0000000 00000000015 11775571156 022373 0 ustar root root launchpadlib
launchpadlib-1.10.2/src/launchpadlib.egg-info/not-zip-safe 0000644 0000000 0000000 00000000001 11775056250 022064 0 ustar root root
launchpadlib-1.10.2/src/launchpadlib.egg-info/requires.txt 0000644 0000000 0000000 00000000146 11775571156 022246 0 ustar root root httplib2
keyring
lazr.restfulclient>=0.9.19
lazr.uri
oauth
setuptools
simplejson
testresources
wadllib launchpadlib-1.10.2/src/launchpadlib/ 0000755 0000000 0000000 00000000000 11775571157 016154 5 ustar root root launchpadlib-1.10.2/src/launchpadlib/apps.py 0000644 0000000 0000000 00000003265 11775055270 017470 0 ustar root root # Copyright 2009 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Command-line applications for Launchpadlib.
This module contains the code for various applications. The applications
themselves are kept in bin/.
"""
__all__ = [
'RequestTokenApp',
'TrustedTokenAuthorizationConsoleApp',
]
import getpass
import sys
import simplejson
from launchpadlib.credentials import (
Credentials, RequestTokenAuthorizationEngine, TokenAuthorizationException)
from launchpadlib.uris import lookup_web_root
class RequestTokenApp(object):
"""An application that creates request tokens."""
def __init__(self, web_root, consumer_name, context):
"""Initialize."""
self.web_root = lookup_web_root(web_root)
self.credentials = Credentials(consumer_name)
self.context = context
def run(self):
"""Get a request token and return JSON information about it."""
token = self.credentials.get_request_token(
self.context, self.web_root,
token_format=Credentials.DICT_TOKEN_FORMAT)
return simplejson.dumps(token)
launchpadlib-1.10.2/src/launchpadlib/NEWS.txt 0000644 0000000 0000000 00000017412 11775570504 017471 0 ustar root root =====================
NEWS for launchpadlib
=====================
1.10.2 (2012-07-05)
===================
- Typo in the doctest fix, discovered when trying to integrate with launchpad
itself. [bug=1020667]
1.10.1 (2012-07-04)
===================
- Fix a doctest in introduction.txt so that the test suite runs with
python-2.7 (note the doctests only run when running integrated with
launchpad's test suite itself). [bug=1020667]
1.10.0 (2012-06-19)
===================
- Add environment variable, LP_DISABLE_SSL_CERTIFICATE_VALIDATION, to
disable SSL certificate checks. Most useful when testing against
development servers.
1.9.12 (2011-12-05)
===================
- Move keyring base64 encoding to KeyringCredential and be more
defensive about decoding. [bug=900307]
1.9.11 (2011-11-21)
===================
- 1.9.10 was a bad release due to incomplete NEWS entries.
- Add fake Launchpad web service for unit test.
- Improve HACKING documentation.
- Improve launchpadlib directory discovery on Windows.
- Added script to delete spurious bugtasks or split a bugtask from a bug.
- Properly handle Unicode passwords if returned by the keyring.
- Base 64 encode serialized credentials before putting in keyring/wallet.
1.9.10 (2011-11-21)
===================
- Base 64 encode serialized credentials before putting in keyring/wallet.
1.9.9 (2011-07-27)
==================
- Fix a failing test for lazr.restfulclient 0.12.0.
1.9.8 (2011-02-28)
==================
- Detect the error Launchpad sends when it doesn't recognize an access
token, and get a new token.
1.9.7 (2011-02-15)
==================
- Slightly tweaked the behavior of EDGE_SERVICE_ROOT, and improved tests.
1.9.6 (2011-02-14)
==================
- Added EDGE_SERVICE_ROOT and the 'edge' alias back, though they both
operate on production behind the scenes. Using the 'edge' alias will
cause a deprecation warning.
1.9.5 (2011-02-08)
==================
- Fixed a bug that prevented the deprecated get_token_and_login code
from working, and that required that users of get_token_and_login
get a new token on every usage.
1.9.4 (2011-01-18)
==================
- Removed references to the 'edge' service root, which is being phased out.
- Fixed a minor bug in the upload_release_tarball contrib script which
was causing tarballs to be uploaded with the wrong media type.
- The XSLT stylesheet for converting the Launchpad WADL into HTML
documentation has been moved back into Launchpad.
1.9.3 (2011-01-10)
==================
- The keyring package import is now delayed until the keyring needs to be
accessed. This reduces launchapdlib users' exposure to unintended side
effects of importing keyring (KWallet authorization dialogs and the
registration of a SIGCHLD handler).
1.9.2 (2011-01-07)
==================
- Added a missing import.
1.9.1 (2011-01-06)
==================
- Corrected a test failure.
1.9.0 (2011-01-05)
==================
- When an authorization token expires or becomes invalid, attempt to
acquire a new one, even in the middle of a session, rather than
crashing.
- The HTML generated by wadl-to-refhtml.xsl now validates.
- Most of the helper login methods have been deprecated. There are now
only two helper methods:
* Launchpad.login_anonymously, for anonymous credential-free access.
* Launchpad.login_with, for programs that need a credential.
1.8.0 (2010-11-15)
==================
- Store authorization tokens in the Gnome keyring or KDE wallet, when
available. The credentials_file parameter of Launchpad.login_with() is now
ignored.
- By default, Launchpad.login_with() now asks Launchpad for
desktop-wide integration. This removes the need for each individual
application to get its own OAuth token.
1.7.0 (2010-09-23)
==================
- Removed "fake Launchpad browser" code that didn't work and was
misleading developers.
- Added support for http://qastaging.launchpad.net by adding
astaging to the uris.
1.6.5 (2010-08-23)
==================
- Make launchpadlib compatible with the latest lazr.restfulclient.
1.6.4 (2010-08-18)
==================
- Test fixes.
1.6.3 (2010-08-12)
==================
- Instead of making the end-user hit Enter after authorizing an
application to access their Launchpad account, launchpadlib will
automatically poll Launchpad until the user makes a decision.
- launchpadlib now raises a more helpful exception when the end-user
explicitly denies access to a launchpadlib application.
- Improved the XSLT stylesheet to reflect Launchpad's more complex
top-level structure. [bug=286941]
- Test fixes. [bug=488448,616055]
1.6.2 (2010-06-21)
==================
- Extended the optimization from version 1.6.1 to apply to Launchpad's
top-level collection of people.
1.6.1 (2010-06-16)
==================
- Added an optimization that lets launchpadlib avoid making an HTTP
request in some situations.
1.6.0 (2010-04-07)
==================
- Fixed a test to work against the latest version of Launchpad.
1.5.8 (2010-03-25)
==================
- Use version 1.0 of the Launchpad web service by default.
1.5.7 (2010-03-16)
==================
- Send a Referer header whenever making requests to the Launchpad
website (as opposed to the web service) to avoid falling afoul of
new cross-site-request-forgery countermeasures.
1.5.6 (2010-03-04)
==================
- Fixed a minor bug when using login_with() to access a version of the
Launchpad web service other than the default.
- Added a check to catch old client code that would cause newer
versions of launchpadlib to make nonsensical requests to
https://api.launchpad.dev/beta/beta/, and raise a helpful exception
telling the developer how to fix it.
1.5.5
=====
- Added the ability to access different versions of the Launchpad web
service.
1.5.4 (2009-12-17)
==================
- Made it easy to get anonymous access to a Launchpad instance.
- Made it easy to plug in different clients that take the user's
Launchpad login and password for purposes of authorizing a request
token. The most secure technique is still the default: to open the
user's web browser to the appropriate Launchpad page.
- Introduced a command-line script bin/launchpad-credentials-console,
which takes the user's Launchpad login and password, and authorizes
a request token on their behalf.
- Introduced a command-line script bin/launchpad-request-token, which
creates a request token on any Launchpad installation and dumps the
JSON description of that token to standard output.
- Shorthand service names like 'edge' should now be respected
everywhere in launchpadlib.
1.5.3 (2009-10-22)
==================
- Moved some more code from launchpadlib into the more generic
lazr.restfulclient.
1.5.2 (2009-10-01)
==================
- Added a number of new sample scripts from elsewhere.
- Added a reference to the production Launchpad instance.
- Made it easier to specify a Launchpad instance to run against.
1.5.1 (2009-07-16)
==================
- Added a sample script for uploading a release tarball to Launchpad.
1.5.0 (2009-07-09)
==================
- Most of launchpadlib's code has been moved to the generic
lazr.restfulclient library. launchpadlib now contains only code
specific to Launchpad. There should be no changes in functionality.
- Moved bootstrap.py into the top-level directory. Having it in a
subdirectory with a top-level symlink was breaking installation on
Windows.
- The notice to the end-user (that we're opening their web
browser) is now better formatted.
1.0.1 (2009-05-30)
==================
- Correct tests for new launchpad cache behavior in librarian
- Remove build dependency on setuptools_bzr because it was causing bzr to be
downloaded during installation of the package, which was unnecessary and
annoying.
1.0 (2009-03-24)
================
- Initial release on PyPI
launchpadlib-1.10.2/src/launchpadlib/credentials.py 0000644 0000000 0000000 00000057222 11775273624 021031 0 ustar root root # Copyright 2008 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""launchpadlib credentials and authentication support."""
__metaclass__ = type
__all__ = [
'AccessToken',
'AnonymousAccessToken',
'AuthorizeRequestTokenWithBrowser',
'CredentialStore',
'RequestTokenAuthorizationEngine',
'Consumer',
'Credentials',
]
import cgi
from cStringIO import StringIO
import httplib2
import os
import stat
import time
from urllib import urlencode
from urlparse import urljoin
import webbrowser
from base64 import (
b64decode,
b64encode,
)
import simplejson
from lazr.restfulclient.errors import HTTPError
from lazr.restfulclient.authorize.oauth import (
AccessToken as _AccessToken,
Consumer,
OAuthAuthorizer,
SystemWideConsumer # Not used directly, just re-imported into here.
)
from launchpadlib import uris
request_token_page = '+request-token'
access_token_page = '+access-token'
authorize_token_page = '+authorize-token'
access_token_poll_time = 1
EXPLOSIVE_ERRORS = (MemoryError, KeyboardInterrupt, SystemExit)
def _ssl_certificate_validation_disabled():
"""Whether the user has disabled SSL certificate connection.
Some testing servers have broken certificates. Rather than raising an
error, we allow an environment variable,
``LP_DISABLE_SSL_CERTIFICATE_VALIDATION`` to disable the check.
"""
# XXX: Copied from lazr/restfulclient/_browser.py. Once it appears in a
# released version of lazr.restfulclient, depend on that new version and
# delete this copy.
return bool(
os.environ.get('LP_DISABLE_SSL_CERTIFICATE_VALIDATION', False))
def _http_post(url, headers, params):
"""POST to ``url`` with ``headers`` and a body of urlencoded ``params``.
Wraps it up to make sure we avoid the SSL certificate validation if our
environment tells us to. Also, raises an error on non-200 statuses.
"""
cert_disabled = _ssl_certificate_validation_disabled()
response, content = httplib2.Http(
disable_ssl_certificate_validation=cert_disabled).request(
url, method='POST', headers=headers, body=urlencode(params))
if response.status != 200:
raise HTTPError(response, content)
return response, content
class Credentials(OAuthAuthorizer):
"""Standard credentials storage and usage class.
:ivar consumer: The consumer (application)
:type consumer: `Consumer`
:ivar access_token: Access information on behalf of the user
:type access_token: `AccessToken`
"""
_request_token = None
URI_TOKEN_FORMAT = "uri"
DICT_TOKEN_FORMAT = "dict"
ITEM_SEPARATOR = '
'
NEWLINE = '\n'
def serialize(self):
"""Turn this object into a string.
This should probably be moved into OAuthAuthorizer.
"""
sio = StringIO()
self.save(sio)
serialized = sio.getvalue()
return serialized
@classmethod
def from_string(cls, value):
"""Create a `Credentials` object from a serialized string.
This should probably be moved into OAuthAuthorizer.
"""
credentials = cls()
credentials.load(StringIO(value))
return credentials
def get_request_token(self, context=None, web_root=uris.STAGING_WEB_ROOT,
token_format=URI_TOKEN_FORMAT):
"""Request an OAuth token to Launchpad.
Also store the token in self._request_token.
This method must not be called on an object with no consumer
specified or if an access token has already been obtained.
:param context: The context of this token, that is, its scope of
validity within Launchpad.
:param web_root: The URL of the website on which the token
should be requested.
:token_format: How the token should be
presented. URI_TOKEN_FORMAT means just return the URL to
the page that authorizes the token. DICT_TOKEN_FORMAT
means return a dictionary describing the token
and the site's authentication policy.
:return: If token_format is URI_TOKEN_FORMAT, the URL for the
user to authorize the `AccessToken` provided by
Launchpad. If token_format is DICT_TOKEN_FORMAT, a dict of
information about the new access token.
"""
assert self.consumer is not None, "Consumer not specified."
assert self.access_token is None, "Access token already obtained."
web_root = uris.lookup_web_root(web_root)
params = dict(
oauth_consumer_key=self.consumer.key,
oauth_signature_method='PLAINTEXT',
oauth_signature='&')
url = web_root + request_token_page
headers = {'Referer': web_root}
if token_format == self.DICT_TOKEN_FORMAT:
headers['Accept'] = 'application/json'
response, content = _http_post(url, headers, params)
if token_format == self.DICT_TOKEN_FORMAT:
params = simplejson.loads(content)
if context is not None:
params["lp.context"] = context
self._request_token = AccessToken.from_params(params)
return params
else:
self._request_token = AccessToken.from_string(content)
url = '%s%s?oauth_token=%s' % (web_root, authorize_token_page,
self._request_token.key)
if context is not None:
self._request_token.context = context
url += "&lp.context=%s" % context
return url
def exchange_request_token_for_access_token(
self, web_root=uris.STAGING_WEB_ROOT):
"""Exchange the previously obtained request token for an access token.
This method must not be called unless get_request_token() has been
called and completed successfully.
The access token will be stored as self.access_token.
:param web_root: The base URL of the website that granted the
request token.
"""
assert self._request_token is not None, (
"get_request_token() doesn't seem to have been called.")
web_root = uris.lookup_web_root(web_root)
params = dict(
oauth_consumer_key=self.consumer.key,
oauth_signature_method='PLAINTEXT',
oauth_token=self._request_token.key,
oauth_signature='&%s' % self._request_token.secret)
url = web_root + access_token_page
headers = {'Referer': web_root}
response, content = _http_post(url, headers, params)
self.access_token = AccessToken.from_string(content)
class AccessToken(_AccessToken):
"""An OAuth access token."""
@classmethod
def from_params(cls, params):
"""Create and return a new `AccessToken` from the given dict."""
key = params['oauth_token']
secret = params['oauth_token_secret']
context = params.get('lp.context')
return cls(key, secret, context)
@classmethod
def from_string(cls, query_string):
"""Create and return a new `AccessToken` from the given string."""
params = cgi.parse_qs(query_string, keep_blank_values=False)
key = params['oauth_token']
assert len(key) == 1, (
"Query string must have exactly one oauth_token.")
key = key[0]
secret = params['oauth_token_secret']
assert len(secret) == 1, "Query string must have exactly one secret."
secret = secret[0]
context = params.get('lp.context')
if context is not None:
assert len(context) == 1, (
"Query string must have exactly one context")
context = context[0]
return cls(key, secret, context)
class AnonymousAccessToken(_AccessToken):
"""An OAuth access token that doesn't authenticate anybody.
This token can be used for anonymous access.
"""
def __init__(self):
super(AnonymousAccessToken, self).__init__('','')
class CredentialStore(object):
"""Store OAuth credentials locally.
This is a generic superclass. To implement a specific way of
storing credentials locally you'll need to subclass this class,
and implement `do_save` and `do_load`.
"""
def __init__(self, credential_save_failed=None):
"""Constructor.
:param credential_save_failed: A callback to be invoked if the
save to local storage fails. You should never invoke this
callback yourself! Instead, you should raise an exception
from do_save().
"""
self.credential_save_failed = credential_save_failed
def save(self, credentials, unique_consumer_id):
"""Save the credentials and invoke the callback on failure.
Do not override this method when subclassing. Override
do_save() instead.
"""
try:
self.do_save(credentials, unique_consumer_id)
except EXPLOSIVE_ERRORS:
raise
except Exception, e:
if self.credential_save_failed is None:
raise e
self.credential_save_failed()
return credentials
def do_save(self, credentials, unique_consumer_id):
"""Store newly-authorized credentials locally for later use.
:param credentials: A Credentials object to save.
:param unique_consumer_id: A string uniquely identifying an
OAuth consumer on a Launchpad instance.
"""
raise NotImplementedError()
def load(self, unique_key):
"""Retrieve credentials from a local store.
This method is the inverse of `save`.
There's no special behavior in this method--it just calls
`do_load`. There _is_ special behavior in `save`, and this
way, developers can remember to implement `do_save` and
`do_load`, not `do_save` and `load`.
:param unique_key: A string uniquely identifying an OAuth consumer
on a Launchpad instance.
:return: A `Credentials` object if one is found in the local
store, and None otherise.
"""
return self.do_load(unique_key)
def do_load(self, unique_key):
"""Retrieve credentials from a local store.
This method is the inverse of `do_save`.
:param unique_key: A string uniquely identifying an OAuth consumer
on a Launchpad instance.
:return: A `Credentials` object if one is found in the local
store, and None otherise.
"""
raise NotImplementedError()
class KeyringCredentialStore(CredentialStore):
"""Store credentials in the GNOME keyring or KDE wallet.
This is a good solution for desktop applications and interactive
scripts. It doesn't work for non-interactive scripts, or for
integrating third-party websites into Launchpad.
"""
B64MARKER = ""
@staticmethod
def _ensure_keyring_imported():
"""Ensure the keyring module is imported (postponing side effects).
The keyring module initializes the environment-dependent backend at
import time (nasty). We want to avoid that initialization because it
may do things like prompt the user to unlock their password store
(e.g., KWallet).
"""
if 'keyring' not in globals():
global keyring
import keyring
def do_save(self, credentials, unique_key):
"""Store newly-authorized credentials in the keyring."""
self._ensure_keyring_imported()
serialized = credentials.serialize()
# Some users have reported problems with corrupted keyrings, both in
# Gnome and KDE, when newlines are included in the password. Avoid
# this problem by base 64 encoding the serialized value.
serialized = self.B64MARKER + b64encode(serialized)
keyring.set_password(
'launchpadlib', unique_key, serialized)
def do_load(self, unique_key):
"""Retrieve credentials from the keyring."""
self._ensure_keyring_imported()
credential_string = keyring.get_password(
'launchpadlib', unique_key)
if credential_string is not None:
credential_string = credential_string.encode('utf8')
if credential_string.startswith(self.B64MARKER):
try:
credential_string = b64decode(
credential_string[len(self.B64MARKER):])
except TypeError:
# The credential_string should be base 64 but cannot be
# decoded.
return None
try:
credentials = Credentials.from_string(credential_string)
return credentials
except:
# If any error occurs at this point the most reasonable thing
# to do is return no credentials, which will require
# re-authorization but the user will be able to proceed.
return None
return None
class UnencryptedFileCredentialStore(CredentialStore):
"""Store credentials unencrypted in a file on disk.
This is a good solution for scripts that need to run without any
user interaction.
"""
def __init__(self, filename, credential_save_failed=None):
super(UnencryptedFileCredentialStore, self).__init__(
credential_save_failed)
self.filename = filename
def do_save(self, credentials, unique_key):
"""Save the credentials to disk."""
credentials.save_to_path(self.filename)
def do_load(self, unique_key):
"""Load the credentials from disk."""
if (os.path.exists(self.filename)
and not os.stat(self.filename)[stat.ST_SIZE] == 0):
return Credentials.load_from_path(self.filename)
return None
class RequestTokenAuthorizationEngine(object):
"""The superclass of all request token authorizers.
This base class does not implement request token authorization,
since that varies depending on how you want the end-user to
authorize a request token. You'll need to subclass this class and
implement `make_end_user_authorize_token`.
"""
UNAUTHORIZED_ACCESS_LEVEL = "UNAUTHORIZED"
def __init__(self, service_root, application_name=None,
consumer_name=None, allow_access_levels=None):
"""Base class initialization.
:param service_root: The root of the Launchpad instance being
used.
:param application_name: The name of the application that
wants to use launchpadlib. This is used in conjunction
with a desktop-wide integration.
If you specify this argument, your values for
consumer_name and allow_access_levels are ignored.
:param consumer_name: The OAuth consumer name, for an
application that wants its own point of integration into
Launchpad. In almost all cases, you want to specify
application_name instead and do a desktop-wide
integration. The exception is when you're integrating a
third-party website into Launchpad.
:param allow_access_levels: A list of the Launchpad access
levels to present to the user. ('READ_PUBLIC' and so on.)
Your value for this argument will be ignored during a
desktop-wide integration.
:type allow_access_levels: A list of strings.
"""
self.service_root = uris.lookup_service_root(service_root)
self.web_root = uris.web_root_for_service_root(service_root)
if application_name is None and consumer_name is None:
raise ValueError(
"You must provide either application_name or consumer_name.")
if application_name is not None and consumer_name is not None:
raise ValueError(
"You must provide only one of application_name and "
"consumer_name. (You provided %r and %r.)" % (
application_name, consumer_name))
if consumer_name is None:
# System-wide integration. Create a system-wide consumer
# and identify the application using a separate
# application name.
allow_access_levels = ["DESKTOP_INTEGRATION"]
consumer = SystemWideConsumer(application_name)
else:
# Application-specific integration. Use the provided
# consumer name to create a consumer automatically.
consumer = Consumer(consumer_name)
application_name = consumer_name
self.consumer = consumer
self.application_name = application_name
self.allow_access_levels = allow_access_levels or []
@property
def unique_consumer_id(self):
"""Return a string identifying this consumer on this host."""
return self.consumer.key + '@' + self.service_root
def authorization_url(self, request_token):
"""Return the authorization URL for a request token.
This is the URL the end-user must visit to authorize the
token. How exactly does this happen? That depends on the
subclass implementation.
"""
page = "%s?oauth_token=%s" % (authorize_token_page, request_token)
allow_permission = "&allow_permission="
if len(self.allow_access_levels) > 0:
page += (
allow_permission
+ allow_permission.join(self.allow_access_levels))
return urljoin(self.web_root, page)
def __call__(self, credentials, credential_store):
"""Authorize a token and associate it with the given credentials.
If the credential store runs into a problem storing the
credential locally, the `credential_save_failed` callback will
be invoked. The callback will not be invoked if there's a
problem authorizing the credentials.
:param credentials: A `Credentials` object. If the end-user
authorizes these credentials, this object will have its
.access_token property set.
:param credential_store: A `CredentialStore` object. If the
end-user authorizes the credentials, they will be
persisted locally using this object.
:return: If the credentials are successfully authorized, the
return value is the `Credentials` object originally passed
in. Otherwise the return value is None.
"""
request_token_string = self.get_request_token(credentials)
# Hand off control to the end-user.
self.make_end_user_authorize_token(credentials, request_token_string)
if credentials.access_token is None:
# The end-user refused to authorize the application.
return None
# save() invokes the callback on failure.
credential_store.save(credentials, self.unique_consumer_id)
return credentials
def get_request_token(self, credentials):
"""Get a new request token from the server.
:param return: The request token.
"""
authorization_json = credentials.get_request_token(
web_root=self.web_root,
token_format=Credentials.DICT_TOKEN_FORMAT)
return authorization_json['oauth_token']
def make_end_user_authorize_token(self, credentials, request_token):
"""Authorize the given request token using the given credentials.
Your subclass must implement this method: it has no default
implementation.
Because an access token may expire or be revoked in the middle
of a session, this method may be called at arbitrary points in
a launchpadlib session, or even multiple times during a single
session (with a different request token each time).
In most cases, however, this method will be called at the
beginning of a launchpadlib session, or not at all.
"""
raise NotImplementedError()
class AuthorizeRequestTokenWithBrowser(RequestTokenAuthorizationEngine):
"""The simplest (and, right now, the only) request token authorizer.
This authorizer simply opens up the end-user's web browser to a
Launchpad URL and lets the end-user authorize the request token
themselves.
"""
WAITING_FOR_USER = "The authorization page:\n (%s)\nshould be opening in your browser. Use your browser to authorize\nthis program to access Launchpad on your behalf. \n\nWaiting to hear from Launchpad about your decision..."
def __init__(self, service_root, application_name, consumer_name=None,
credential_save_failed=None, allow_access_levels=None):
"""Constructor.
:param service_root: See `RequestTokenAuthorizationEngine`.
:param application_name: See `RequestTokenAuthorizationEngine`.
:param consumer_name: The value of this argument is
ignored. If we have the capability to open the end-user's
web browser, we must be running on the end-user's computer,
so we should do a full desktop integration.
:param credential_save_failed: See `RequestTokenAuthorizationEngine`.
:param allow_access_levels: The value of this argument is
ignored, for the same reason as consumer_name.
"""
# It doesn't look like we're doing anything here, but we
# are discarding the passed-in values for consumer_name and
# allow_access_levels.
super(AuthorizeRequestTokenWithBrowser, self).__init__(
service_root, application_name, None,
credential_save_failed)
def output(self, message):
"""Display a message.
By default, prints the message to standard output. The message
does not require any user interaction--it's solely
informative.
"""
print message
def make_end_user_authorize_token(self, credentials, request_token):
"""Have the end-user authorize the token in their browser."""
authorization_url = self.authorization_url(request_token)
webbrowser.open(authorization_url)
self.output(self.WAITING_FOR_USER % authorization_url)
while credentials.access_token is None:
time.sleep(access_token_poll_time)
try:
credentials.exchange_request_token_for_access_token(
self.web_root)
break
except HTTPError, e:
if e.response.status == 403:
# The user decided not to authorize this
# application.
raise EndUserDeclinedAuthorization(e.content)
elif e.response.status == 401:
# The user has not made a decision yet.
pass
else:
# There was an error accessing the server.
print "Unexpected response from Launchpad:"
print e
class TokenAuthorizationException(Exception):
pass
class RequestTokenAlreadyAuthorized(TokenAuthorizationException):
pass
class EndUserDeclinedAuthorization(TokenAuthorizationException):
pass
class ClientError(TokenAuthorizationException):
pass
class ServerError(TokenAuthorizationException):
pass
class NoLaunchpadAccount(TokenAuthorizationException):
pass
class TooManyAuthenticationFailures(TokenAuthorizationException):
pass
launchpadlib-1.10.2/src/launchpadlib/uris.py 0000644 0000000 0000000 00000010502 11775055270 017477 0 ustar root root # Copyright 2009 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Launchpad-specific URIs and convenience lookup functions.
The code in this module lets users say "staging" when they mean
"https://api.staging.launchpad.net/".
"""
__metaclass__ = type
__all__ = [
'lookup_service_root',
'lookup_web_root',
'web_root_for_service_root',
]
from urlparse import urlparse
import warnings
from lazr.uri import URI
LPNET_SERVICE_ROOT = 'https://api.launchpad.net/'
QASTAGING_SERVICE_ROOT = 'https://api.qastaging.launchpad.net/'
STAGING_SERVICE_ROOT = 'https://api.staging.launchpad.net/'
DEV_SERVICE_ROOT = 'https://api.launchpad.dev/'
DOGFOOD_SERVICE_ROOT = 'https://api.dogfood.launchpad.net/'
TEST_DEV_SERVICE_ROOT = 'http://api.launchpad.dev:8085/'
LPNET_WEB_ROOT = 'https://launchpad.net/'
QASTAGING_WEB_ROOT = 'https://qastaging.launchpad.net/'
STAGING_WEB_ROOT = 'https://staging.launchpad.net/'
DEV_WEB_ROOT = 'https://launchpad.dev/'
DOGFOOD_WEB_ROOT = 'https://dogfood.launchpad.net/'
TEST_DEV_WEB_ROOT = 'http://launchpad.dev:8085/'
# If you use EDGE_SERVICE_ROOT, or its alias, or the equivalent
# string, launchpadlib will issue a deprecation warning and use
# PRODUCTION_SERVICE_ROOT instead. Similarly for EDGE_WEB_ROOT.
EDGE_SERVICE_ROOT = 'https://api.edge.launchpad.net/'
EDGE_WEB_ROOT = 'https://edge.launchpad.net/'
service_roots = dict(
production=LPNET_SERVICE_ROOT,
edge=LPNET_SERVICE_ROOT,
qastaging=QASTAGING_SERVICE_ROOT,
staging=STAGING_SERVICE_ROOT,
dogfood=DOGFOOD_SERVICE_ROOT,
dev=DEV_SERVICE_ROOT,
test_dev=TEST_DEV_SERVICE_ROOT
)
web_roots = dict(
production=LPNET_WEB_ROOT,
edge = LPNET_WEB_ROOT,
qastaging=QASTAGING_WEB_ROOT,
staging=STAGING_WEB_ROOT,
dogfood=DOGFOOD_WEB_ROOT,
dev=DEV_WEB_ROOT,
test_dev=TEST_DEV_WEB_ROOT
)
def _dereference_alias(root, aliases):
"""Dereference what might a URL or an alias for a URL."""
if root == 'edge':
warnings.warn(("Launchpad edge server no longer exists. "
"Using 'production' instead."), DeprecationWarning)
if root in aliases:
return aliases[root]
# It's not an alias. Is it a valid URL?
(scheme, netloc, path, parameters, query, fragment) = urlparse(root)
if scheme != "" and netloc != "":
return root
# It's not an alias or a valid URL.
raise ValueError("%s is not a valid URL or an alias for any Launchpad "
"server" % root)
def lookup_service_root(service_root):
"""Dereference an alias to a service root.
A recognized server alias such as "staging" gets turned into the
appropriate URI. A URI gets returned as is. Any other string raises a
ValueError.
"""
if service_root == EDGE_SERVICE_ROOT:
# This will trigger a deprecation warning and use production instead.
service_root = 'edge'
return _dereference_alias(service_root, service_roots)
def lookup_web_root(web_root):
"""Dereference an alias to a website root.
A recognized server alias such as "staging" gets turned into the
appropriate URI. A URI gets returned as is. Any other string raises a
ValueError.
"""
if web_root == EDGE_WEB_ROOT:
# This will trigger a deprecation warning and use production instead.
web_root = 'edge'
return _dereference_alias(web_root, web_roots)
def web_root_for_service_root(service_root):
"""Turn a service root URL into a web root URL.
This is done heuristically, not with a lookup.
"""
service_root = lookup_service_root(service_root)
web_root_uri = URI(service_root)
web_root_uri.path = ""
web_root_uri.host = web_root_uri.host.replace("api.", "", 1)
web_root = str(web_root_uri.ensureSlash())
return web_root
launchpadlib-1.10.2/src/launchpadlib/errors.py 0000644 0000000 0000000 00000001421 11775055270 020031 0 ustar root root # Copyright 2008 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Reimport errors from restfulclient for convenience's sake."""
from lazr.restfulclient.errors import *
launchpadlib-1.10.2/src/launchpadlib/__init__.py 0000644 0000000 0000000 00000001302 11775570504 020254 0 ustar root root # Copyright 2008-2011 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
__version__ = '1.10.2'
launchpadlib-1.10.2/src/launchpadlib/testing/ 0000755 0000000 0000000 00000000000 11775571157 017631 5 ustar root root launchpadlib-1.10.2/src/launchpadlib/testing/__init__.py 0000644 0000000 0000000 00000000000 11775055270 021721 0 ustar root root launchpadlib-1.10.2/src/launchpadlib/testing/tests/ 0000755 0000000 0000000 00000000000 11775571157 020773 5 ustar root root launchpadlib-1.10.2/src/launchpadlib/testing/tests/__init__.py 0000644 0000000 0000000 00000000000 11775055270 023063 0 ustar root root launchpadlib-1.10.2/src/launchpadlib/testing/tests/test_launchpad.py 0000644 0000000 0000000 00000037125 11775055270 024344 0 ustar root root # Copyright 2008 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# launchpadlib is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with launchpadlib. If not, see
# .
from datetime import datetime
from testresources import ResourcedTestCase
from launchpadlib.testing.launchpad import (
FakeLaunchpad,
FakeResource,
FakeRoot,
IntegrityError,
)
from launchpadlib.testing.resources import (
FakeLaunchpadResource, get_application)
class FakeRootTest(ResourcedTestCase):
def test_create_root_resource(self):
root_resource = FakeRoot(get_application())
self.assertTrue(isinstance(root_resource, FakeResource))
class FakeResourceTest(ResourcedTestCase):
resources = [("launchpad", FakeLaunchpadResource())]
def test_repr(self):
"""A custom C{__repr__} is provided for L{FakeResource}s."""
branches = dict(total_size="test-branch")
self.launchpad.me = dict(getBranches=lambda statuses: branches)
branches = self.launchpad.me.getBranches([])
obj_id = hex(id(branches))
self.assertEqual(
"" % obj_id,
repr(branches))
def test_repr_with_name(self):
"""
If the fake has a C{name} property it's included in the repr string to
make it easier to figure out what it is.
"""
self.launchpad.me = dict(name="foo")
person = self.launchpad.me
self.assertEqual("" % hex(id(person)),
repr(person))
def test_repr_with_id(self):
"""
If the fake has an C{id} property it's included in the repr string to
make it easier to figure out what it is.
"""
bug = dict(id="1", title="Bug #1")
self.launchpad.bugs = dict(entries=[bug])
[bug] = list(self.launchpad.bugs)
self.assertEqual("" % hex(id(bug)), repr(bug))
class FakeLaunchpadTest(ResourcedTestCase):
resources = [("launchpad", FakeLaunchpadResource())]
def test_wb_instantiate_without_application(self):
"""
The builtin WADL definition is used if the C{application} is not
provided during instantiation.
"""
credentials = object()
launchpad = FakeLaunchpad(credentials)
self.assertEqual(credentials, launchpad.credentials)
self.assertEqual(get_application(), launchpad._application)
def test_instantiate_with_everything(self):
"""
L{FakeLaunchpad} takes the same parameters as L{Launchpad} during
instantiation, with the addition of an C{application} parameter. The
optional parameters are discarded when the object is instantiated.
"""
credentials = object()
launchpad = FakeLaunchpad(credentials, service_root=None, cache=None,
timeout=None, proxy_info=None,
application=get_application())
self.assertEqual(credentials, launchpad.credentials)
def test_instantiate_with_credentials(self):
"""A L{FakeLaunchpad} can be instantiated with credentials."""
credentials = object()
launchpad = FakeLaunchpad(credentials, application=get_application())
self.assertEqual(credentials, launchpad.credentials)
def test_instantiate_without_credentials(self):
"""
A L{FakeLaunchpad} instantiated without credentials has its
C{credentials} attribute set to C{None}.
"""
self.assertEqual(None, self.launchpad.credentials)
def test_set_undefined_property(self):
"""
An L{IntegrityError} is raised if an attribute is set on a
L{FakeLaunchpad} instance that isn't present in the WADL definition.
"""
self.assertRaises(IntegrityError, setattr, self.launchpad, "foo", "bar")
def test_get_undefined_resource(self):
"""
An L{AttributeError} is raised if an attribute is accessed on a
L{FakeLaunchpad} instance that doesn't exist.
"""
self.launchpad.me = dict(display_name="Foo")
self.assertRaises(AttributeError, getattr, self.launchpad.me, "name")
def test_string_property(self):
"""
Sample data can be created by setting L{FakeLaunchpad} attributes with
dicts that represent objects. Plain string values can be represented
as C{str} values.
"""
self.launchpad.me = dict(name="foo")
self.assertEqual("foo", self.launchpad.me.name)
def test_unicode_property(self):
"""
Sample data can be created by setting L{FakeLaunchpad} attributes with
dicts that represent objects. Plain string values can be represented
as C{unicode} strings.
"""
self.launchpad.me = dict(name=u"foo")
self.assertEqual(u"foo", self.launchpad.me.name)
def test_datetime_property(self):
"""
Attributes that represent dates are set with C{datetime} instances.
"""
now = datetime.utcnow()
self.launchpad.me = dict(date_created=now)
self.assertEqual(now, self.launchpad.me.date_created)
def test_invalid_datetime_property(self):
"""
Only C{datetime} values can be set on L{FakeLaunchpad} instances for
attributes that represent dates.
"""
self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
dict(date_created="now"))
def test_multiple_string_properties(self):
"""
Sample data can be created by setting L{FakeLaunchpad} attributes with
dicts that represent objects.
"""
self.launchpad.me = dict(name="foo", display_name="Foo")
self.assertEqual("foo", self.launchpad.me.name)
self.assertEqual("Foo", self.launchpad.me.display_name)
def test_invalid_property_name(self):
"""
Sample data set on a L{FakeLaunchpad} instance is validated against
the WADL definition. If a key is defined on a resource that doesn't
match a related parameter, an L{IntegrityError} is raised.
"""
self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
dict(foo="bar"))
def test_invalid_property_value(self):
"""
The types of sample data values set on L{FakeLaunchpad} instances are
validated against types defined in the WADL definition.
"""
self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
dict(name=102))
def test_callable(self):
"""
A callable set on a L{FakeLaunchpad} instance is validated against the
WADL definition, to make sure a matching method exists.
"""
branches = dict(total_size="test-branch")
self.launchpad.me = dict(getBranches=lambda statuses: branches)
self.assertNotEqual(None, self.launchpad.me.getBranches([]))
def test_invalid_callable_name(self):
"""
An L{IntegrityError} is raised if a method is defined on a resource
that doesn't match a method defined in the WADL definition.
"""
self.assertRaises(IntegrityError, setattr, self.launchpad, "me",
dict(foo=lambda: None))
def test_callable_object_return_type(self):
"""
The result of a fake method is a L{FakeResource}, automatically
created from the object used to define the return object.
"""
branches = dict(total_size="8")
self.launchpad.me = dict(getBranches=lambda statuses: branches)
branches = self.launchpad.me.getBranches([])
self.assertTrue(isinstance(branches, FakeResource))
self.assertEqual("8", branches.total_size)
def test_invalid_callable_object_return_type(self):
"""
An L{IntegrityError} is raised if a method returns an invalid result.
"""
branches = dict(total_size=8)
self.launchpad.me = dict(getBranches=lambda statuses: branches)
self.assertRaises(IntegrityError, self.launchpad.me.getBranches, [])
def test_collection_property(self):
"""
Sample collections can be set on L{FakeLaunchpad} instances. They are
validated the same way other sample data is validated.
"""
branch = dict(name="foo")
self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
branch = self.launchpad.branches.getByUniqueName("foo")
self.assertEqual("foo", branch.name)
def test_iterate_collection(self):
"""
Data for a sample collection set on a L{FakeLaunchpad} instance can be
iterated over if an C{entries} key is defined.
"""
bug = dict(id="1", title="Bug #1")
self.launchpad.bugs = dict(entries=[bug])
bugs = [bug for bug in self.launchpad.bugs]
self.assertEqual(1, len(bugs))
bug = bugs[0]
self.assertEqual("1", bug.id)
self.assertEqual("Bug #1", bug.title)
def test_collection_with_invalid_entries(self):
"""
Sample data for each entry in a collection is validated when it's set
on a L{FakeLaunchpad} instance.
"""
bug = dict(foo="bar")
self.assertRaises(IntegrityError, setattr, self.launchpad, "bugs",
dict(entries=[bug]))
def test_slice_collection(self):
"""
Data for a sample collection set on a L{FakeLaunchpad} instance can be
sliced if an C{entries} key is defined.
"""
bug1 = dict(id="1", title="Bug #1")
bug2 = dict(id="2", title="Bug #2")
bug3 = dict(id="3", title="Bug #3")
self.launchpad.bugs = dict(entries=[bug1, bug2, bug3])
bugs = self.launchpad.bugs[1:3]
self.assertEqual(2, len(bugs))
self.assertEqual("2", bugs[0].id)
self.assertEqual("3", bugs[1].id)
def test_slice_collection_with_negative_start(self):
"""
A C{ValueError} is raised if a negative start value is used when
slicing a sample collection set on a L{FakeLaunchpad} instance.
"""
bug1 = dict(id="1", title="Bug #1")
bug2 = dict(id="2", title="Bug #2")
self.launchpad.bugs = dict(entries=[bug1, bug2])
self.assertRaises(ValueError, lambda: self.launchpad.bugs[-1:])
self.assertRaises(ValueError, lambda: self.launchpad.bugs[-1:2])
def test_slice_collection_with_negative_stop(self):
"""
A C{ValueError} is raised if a negative stop value is used when
slicing a sample collection set on a L{FakeLaunchpad} instance.
"""
bug1 = dict(id="1", title="Bug #1")
bug2 = dict(id="2", title="Bug #2")
self.launchpad.bugs = dict(entries=[bug1, bug2])
self.assertRaises(ValueError, lambda: self.launchpad.bugs[:-1])
self.assertRaises(ValueError, lambda: self.launchpad.bugs[0:-1])
def test_subscript_operator_out_of_range(self):
"""
An C{IndexError} is raised if an invalid index is used when retrieving
data from a sample collection.
"""
bug1 = dict(id="1", title="Bug #1")
self.launchpad.bugs = dict(entries=[bug1])
self.assertRaises(IndexError, lambda: self.launchpad.bugs[2])
def test_replace_property(self):
"""Values already set on fake resource objects can be replaced."""
self.launchpad.me = dict(name="foo")
person = self.launchpad.me
self.assertEqual("foo", person.name)
person.name = "bar"
self.assertEqual("bar", person.name)
self.assertEqual("bar", self.launchpad.me.name)
def test_replace_method(self):
"""Methods already set on fake resource objects can be replaced."""
branch1 = dict(name="foo", bzr_identity="lp:~user/project/branch1")
branch2 = dict(name="foo", bzr_identity="lp:~user/project/branch2")
self.launchpad.branches = dict(getByUniqueName=lambda name: branch1)
self.launchpad.branches.getByUniqueName = lambda name: branch2
branch = self.launchpad.branches.getByUniqueName("foo")
self.assertEqual("lp:~user/project/branch2", branch.bzr_identity)
def test_replace_property_with_invalid_value(self):
"""Values set on fake resource objects are validated."""
self.launchpad.me = dict(name="foo")
person = self.launchpad.me
self.assertRaises(IntegrityError, setattr, person, "name", 1)
def test_replace_resource(self):
"""Resources already set on L{FakeLaunchpad} can be replaced."""
self.launchpad.me = dict(name="foo")
self.assertEqual("foo", self.launchpad.me.name)
self.launchpad.me = dict(name="bar")
self.assertEqual("bar", self.launchpad.me.name)
def test_add_property(self):
"""Sample data set on a L{FakeLaunchpad} instance can be added to."""
self.launchpad.me = dict(name="foo")
person = self.launchpad.me
person.display_name = "Foo"
self.assertEqual("foo", person.name)
self.assertEqual("Foo", person.display_name)
self.assertEqual("foo", self.launchpad.me.name)
self.assertEqual("Foo", self.launchpad.me.display_name)
def test_add_property_to_empty_object(self):
"""An empty object can be used when creating sample data."""
self.launchpad.me = dict()
self.assertRaises(AttributeError, getattr, self.launchpad.me, "name")
self.launchpad.me.name = "foo"
self.assertEqual("foo", self.launchpad.me.name)
def test_login(self):
"""
L{FakeLaunchpad.login} ignores all parameters and returns a new
instance using the builtin WADL definition.
"""
launchpad = FakeLaunchpad.login("name", "token", "secret")
self.assertTrue(isinstance(launchpad, FakeLaunchpad))
def test_get_token_and_login(self):
"""
L{FakeLaunchpad.get_token_and_login} ignores all parameters and
returns a new instance using the builtin WADL definition.
"""
launchpad = FakeLaunchpad.get_token_and_login("name")
self.assertTrue(isinstance(launchpad, FakeLaunchpad))
def test_login_with(self):
"""
L{FakeLaunchpad.login_with} ignores all parameters and returns a new
instance using the builtin WADL definition.
"""
launchpad = FakeLaunchpad.login_with("name")
self.assertTrue(isinstance(launchpad, FakeLaunchpad))
def test_lp_save(self):
"""
Sample object have an C{lp_save} method that is a no-op by default.
"""
self.launchpad.me = dict(name="foo")
self.assertTrue(self.launchpad.me.lp_save())
def test_custom_lp_save(self):
"""A custom C{lp_save} method can be set on a L{FakeResource}."""
self.launchpad.me = dict(name="foo", lp_save=lambda: "custom")
self.assertEqual("custom", self.launchpad.me.lp_save())
def test_set_custom_lp_save(self):
"""
A custom C{lp_save} method can be set on a L{FakeResource} after its
been created.
"""
self.launchpad.me = dict(name="foo")
self.launchpad.me.lp_save = lambda: "custom"
self.assertEqual("custom", self.launchpad.me.lp_save())
launchpadlib-1.10.2/src/launchpadlib/testing/resources.py 0000644 0000000 0000000 00000003720 11775055270 022210 0 ustar root root # Copyright 2008, 2011 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# launchpadlib is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with launchpadlib. If not, see
# .
"""Resources for use in unit tests with the C{testresources} module."""
from pkg_resources import resource_string
from testresources import TestResource
from wadllib.application import Application
from launchpadlib.testing.launchpad import FakeLaunchpad
launchpad_testing_application = None
def get_application():
"""Get or create a WADL application for testing Launchpad.
Note that this uses the Launchpad v1.0 WADL bundled with launchpadlib for
testing purposes. For your own application, you might want to construct
an L{Application} object directly, giving it your own WADL.
"""
global launchpad_testing_application
if launchpad_testing_application is None:
markup_url = "https://api.launchpad.net/1.0/"
markup = resource_string("launchpadlib.testing",
"launchpad-wadl.xml")
launchpad_testing_application = Application(markup_url, markup)
return launchpad_testing_application
class FakeLaunchpadResource(TestResource):
def make(self, dependency_resources):
return FakeLaunchpad(
application=Application(
"https://api.example.com/testing/",
resource_string("launchpadlib.testing", "testing-wadl.xml")))
launchpadlib-1.10.2/src/launchpadlib/testing/helpers.py 0000644 0000000 0000000 00000014562 11775055270 021646 0 ustar root root # Copyright 2008 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# launchpadlib is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with launchpadlib. If not, see
# .
"""launchpadlib testing helpers."""
__metaclass__ = type
__all__ = [
'BadSaveKeyring',
'fake_keyring',
'FauxSocketModule',
'InMemoryKeyring',
'NoNetworkAuthorizationEngine',
'NoNetworkLaunchpad',
'TestableLaunchpad',
'nopriv_read_nonprivate',
'salgado_read_nonprivate',
'salgado_with_full_permissions',
]
from contextlib import contextmanager
import launchpadlib
from launchpadlib.launchpad import Launchpad
from launchpadlib.credentials import (
AccessToken,
Credentials,
RequestTokenAuthorizationEngine,
)
missing = object()
def assert_keyring_not_imported():
assert getattr(launchpadlib.credentials, 'keyring', missing) is missing, (
'During tests the real keyring module should never be imported.')
class NoNetworkAuthorizationEngine(RequestTokenAuthorizationEngine):
"""An authorization engine that doesn't open a web browser.
You can use this to test the creation of Launchpad objects and the
storing of credentials. You can't use it to interact with the web
service, since it only pretends to authorize its OAuth request tokens.
"""
ACCESS_TOKEN_KEY = "access_key:84"
def __init__(self, *args, **kwargs):
super(NoNetworkAuthorizationEngine, self).__init__(*args, **kwargs)
# Set up some instrumentation.
self.request_tokens_obtained = 0
self.access_tokens_obtained = 0
def get_request_token(self, credentials):
"""Pretend to get a request token from the server.
We do this by simply returning a static token ID.
"""
self.request_tokens_obtained += 1
return "request_token:42"
def make_end_user_authorize_token(self, credentials, request_token):
"""Pretend to exchange a request token for an access token.
We do this by simply setting the access_token property.
"""
credentials.access_token = AccessToken(
self.ACCESS_TOKEN_KEY, 'access_secret:168')
self.access_tokens_obtained += 1
class NoNetworkLaunchpad(Launchpad):
"""A Launchpad instance for tests with no network access.
It's only useful for making sure that certain methods were called.
It can't be used to interact with the API.
"""
def __init__(self, credentials, authorization_engine, credential_store,
service_root, cache, timeout, proxy_info, version):
self.credentials = credentials
self.authorization_engine = authorization_engine
self.credential_store = credential_store
self.passed_in_args = dict(
service_root=service_root, cache=cache, timeout=timeout,
proxy_info=proxy_info, version=version)
@classmethod
def authorization_engine_factory(cls, *args):
return NoNetworkAuthorizationEngine(*args)
class TestableLaunchpad(Launchpad):
"""A base class for talking to the testing root service."""
def __init__(self, credentials, authorization_engine=None,
credential_store=None, service_root="test_dev",
cache=None, timeout=None, proxy_info=None,
version=Launchpad.DEFAULT_VERSION):
"""Provide test-friendly defaults.
:param authorization_engine: Defaults to None, since a test
environment can't use an authorization engine.
:param credential_store: Defaults to None, since tests
generally pass in fully-formed Credentials objects.
:param service_root: Defaults to 'test_dev'.
"""
super(TestableLaunchpad, self).__init__(
credentials, authorization_engine, credential_store,
service_root=service_root, cache=cache, timeout=timeout,
proxy_info=proxy_info, version=version)
@contextmanager
def fake_keyring(fake):
"""A context manager which injects a testing keyring implementation."""
# The real keyring package should never be imported during tests.
assert_keyring_not_imported()
launchpadlib.credentials.keyring = fake
try:
yield
finally:
del launchpadlib.credentials.keyring
class FauxSocketModule:
"""A socket module replacement that provides a fake hostname."""
def gethostname(self):
return 'HOSTNAME'
class BadSaveKeyring:
"""A keyring that generates errors when saving passwords."""
def get_password(self, service, username):
return None
def set_password(self, service, username, password):
raise RuntimeError
class InMemoryKeyring:
"""A keyring that saves passwords only in memory."""
def __init__(self):
self.data = {}
def set_password(self, service, username, password):
self.data[service, username] = password
def get_password(self, service, username):
return self.data.get((service, username))
class KnownTokens:
"""Known access token/secret combinations."""
def __init__(self, token_string, access_secret):
self.token_string = token_string
self.access_secret = access_secret
self.token = AccessToken(token_string, access_secret)
self.credentials = Credentials(
consumer_name="launchpad-library", access_token=self.token)
def login(self, cache=None, timeout=None, proxy_info=None,
version=Launchpad.DEFAULT_VERSION):
"""Create a Launchpad object using these credentials."""
return TestableLaunchpad(
self.credentials, cache=cache, timeout=timeout,
proxy_info=proxy_info, version=version)
salgado_with_full_permissions = KnownTokens('salgado-change-anything', 'test')
salgado_read_nonprivate = KnownTokens('salgado-read-nonprivate', 'secret')
nopriv_read_nonprivate = KnownTokens('nopriv-read-nonprivate', 'mystery')
launchpadlib-1.10.2/src/launchpadlib/testing/launchpad.py 0000644 0000000 0000000 00000052657 11775055270 022152 0 ustar root root # Copyright 2008 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# launchpadlib is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with launchpadlib. If not, see
# .
"""Testing API allows fake data to be used in unit tests.
Testing launchpadlib code is tricky, because it depends so heavily on a
remote, unique webservice: Launchpad. This module helps you write tests for
your launchpadlib application that can be run locally and quickly.
Say you were writing some code that needed to call out to Launchpad and get
the branches owned by the logged-in person, and then do something to them. For
example, something like this::
def collect_unique_names(lp):
names = []
for branch in lp.me.getBranches():
names.append(branch.unique_name)
return names
To test it, you would first prepare a L{FakeLaunchpad} object, and give it
some sample data of your own devising::
lp = FakeLaunchpad()
my_branches = [dict(unique_name='~foo/bar/baz')]
lp.me = dict(getBranches: lambda status: my_branches)
Then, in the test, call your own code and assert that it behaves correctly
given the data.
names = collect_unique_names(lp)
self.assertEqual(['~foo/bar/baz'], names)
And that's it.
The L{FakeLaunchpad} code uses a WADL file to type-check any objects created
or returned. This means you can be sure that you won't accidentally store
sample data with misspelled attribute names.
The WADL file that we use by default is for version 1.0 of the Launchpad API.
If you want to work against a more recent version of the API, download the
WADL yourself (see ) and construct
your C{FakeLaunchpad} like this::
from wadllib.application import Application
lp = FakeLaunchpad(
Application('https://api.launchpad.net/devel/',
'/path/to/wadl.xml'))
Where 'https://api.launchpad.net/devel/' is the URL for the WADL file, found
also in the WADL file itelf.
"""
from datetime import datetime
JSON_MEDIA_TYPE = "application/json"
class IntegrityError(Exception):
"""Raised when bad sample data is used with a L{FakeLaunchpad} instance."""
class FakeLaunchpad(object):
"""A fake Launchpad API class for unit tests that depend on L{Launchpad}.
@param application: A C{wadllib.application.Application} instance for a
Launchpad WADL definition file.
"""
def __init__(self, credentials=None, service_root=None, cache=None,
timeout=None, proxy_info=None, application=None):
if application is None:
from launchpadlib.testing.resources import get_application
application = get_application()
root_resource = FakeRoot(application)
self.__dict__.update({"credentials": credentials,
"_application": application,
"_service_root": root_resource})
def __setattr__(self, name, values):
"""Set sample data.
@param name: The name of the attribute.
@param values: A dict representing an object matching a resource
defined in Launchpad's WADL definition.
"""
service_root = self._service_root
setattr(service_root, name, values)
def __getattr__(self, name):
"""Get sample data.
@param name: The name of the attribute.
"""
return getattr(self._service_root, name)
@classmethod
def login(cls, consumer_name, token_string, access_secret,
service_root=None, cache=None, timeout=None, proxy_info=None):
"""Convenience for setting up access credentials."""
from launchpadlib.testing.resources import get_application
return cls(object(), application=get_application())
@classmethod
def get_token_and_login(cls, consumer_name, service_root=None,
cache=None, timeout=None, proxy_info=None):
"""Get credentials from Launchpad and log into the service root."""
from launchpadlib.testing.resources import get_application
return cls(object(), application=get_application())
@classmethod
def login_with(cls, consumer_name, service_root=None,
launchpadlib_dir=None, timeout=None, proxy_info=None):
"""Log in to Launchpad with possibly cached credentials."""
from launchpadlib.testing.resources import get_application
return cls(object(), application=get_application())
def find_by_attribute(element, name, value):
"""Find children of 'element' where attribute 'name' is equal to 'value'.
"""
return [child for child in element if child.get(name) == value]
def strip_suffix(string, suffix):
if string.endswith(suffix):
return string[:-len(suffix)]
return string
class FakeResource(object):
"""
Represents valid sample data on L{FakeLaunchpad} instances.
@ivar _children: A dictionary of child resources, each of type
C{FakeResource}.
@ivar _values: A dictionary of values associated with this resource. e.g.
"display_name" or "date_created". The values of this dictionary will
never be C{FakeResource}s.
Note that if C{_children} has a key, then C{_values} will not, and vice
versa. That is, they are distinct dicts.
"""
special_methods = ["lp_save"]
def __init__(self, application, resource_type, values=None):
"""Construct a FakeResource.
@param application: A C{waddlib.application.Application} instance.
@param resource_type: A C{wadllib.application.ResourceType} instance
for this resource.
@param values: Optionally, a dict representing attribute key/value
pairs for this resource.
"""
if values is None:
values = {}
self.__dict__.update({"_application": application,
"_resource_type": resource_type,
"_children": {},
"_values": values})
def __setattr__(self, name, value):
"""Set sample data.
C{value} can be a dict representing an object matching a resource
defined in the WADL definition. Alternatively, C{value} could be a
resource itself. Either way, it is checked for type correctness
against the WADL definition.
"""
if isinstance(value, dict):
self._children[name] = self._create_child_resource(name, value)
else:
values = {}
values.update(self._values)
values[name] = value
# Confirm that the new 'values' dict is a partial type match for
# this resource.
self._check_resource_type(self._resource_type, values)
self.__dict__["_values"] = values
def __getattr__(self, name, _marker=object()):
"""Get sample data.
@param name: The name of the attribute.
"""
result = self._children.get(name, _marker)
if result is _marker:
result = self._values.get(name, _marker)
if callable(result):
return self._wrap_method(name, result)
if name in self.special_methods:
return lambda: True
if result is _marker:
raise AttributeError("%r has no attribute '%s'" % (self, name))
return result
def _wrap_method(self, name, method):
"""Wrapper around methods validates results when it's run.
@param name: The name of the method.
@param method: The callable to run when the method is called.
"""
def wrapper(*args, **kwargs):
return self._run_method(name, method, *args, **kwargs)
return wrapper
def _create_child_resource(self, name, values):
"""
Ensure that C{values} is a valid object for the C{name} attribute and
return a resource object to represent it as API data.
@param name: The name of the attribute to check the C{values} object
against.
@param values: A dict with key/value pairs representing attributes and
methods of an object matching the C{name} resource's definition.
@return: A L{FakeEntry} for an ordinary resource or a
L{FakeCollection} for a resource that represents a collection.
@raises IntegrityError: Raised if C{name} isn't a valid attribute for
this resource or if C{values} isn't a valid object for the C{name}
attribute.
"""
root_resource = self._application.get_resource_by_path("")
is_link = False
param = root_resource.get_parameter(name + "_collection_link",
JSON_MEDIA_TYPE)
if param is None:
is_link = True
param = root_resource.get_parameter(name + "_link", JSON_MEDIA_TYPE)
if param is None:
raise IntegrityError("%s isn't a valid property." % (name,))
resource_type = self._get_resource_type(param)
if is_link:
self._check_resource_type(resource_type, values)
return FakeEntry(self._application, resource_type, values)
else:
name, child_resource_type = (
self._check_collection_type(resource_type, values))
return FakeCollection(self._application, resource_type, values,
name, child_resource_type)
def _get_resource_type(self, param):
"""Get the resource type for C{param}.
@param param: An object representing a C{_link} or C{_collection_link}
parameter.
@return: The resource type for the parameter, or None if one isn't
available.
"""
[link] = list(param.tag)
name = link.get("resource_type")
return self._application.get_resource_type(name)
def _check_resource_type(self, resource_type, partial_object):
"""
Ensure that attributes and methods defined for C{partial_object} match
attributes and methods defined for C{resource_type}.
@param resource_type: The resource type to check the attributes and
methods against.
@param partial_object: A dict with key/value pairs representing
attributes and methods.
"""
for name, value in partial_object.iteritems():
if callable(value):
# Performs an integrity check.
self._get_method(resource_type, name)
else:
self._check_attribute(resource_type, name, value)
def _check_collection_type(self, resource_type, partial_object):
"""
Ensure that attributes and methods defined for C{partial_object} match
attributes and methods defined for C{resource_type}. Collection
entries are treated specially.
@param resource_type: The resource type to check the attributes and
methods against.
@param partial_object: A dict with key/value pairs representing
attributes and methods.
@return: (name, resource_type), where 'name' is the name of the child
resource type and 'resource_type' is the corresponding resource
type.
"""
name = None
child_resource_type = None
for name, value in partial_object.iteritems():
if name == "entries":
name, child_resource_type = (
self._check_entries(resource_type, value))
elif callable(value):
# Performs an integrity check.
self._get_method(resource_type, name)
else:
self._check_attribute(resource_type, name, value)
return name, child_resource_type
def _find_representation_id(self, resource_type, name):
"""Find the WADL XML id for the representation of C{resource_type}.
Looks in the WADL for the first representiation associated with the
method for a resource type.
:return: An XML id (a string).
"""
get_method = self._get_method(resource_type, name)
for response in get_method:
for representation in response:
representation_url = representation.get("href")
if representation_url is not None:
return self._application.lookup_xml_id(representation_url)
def _check_attribute(self, resource_type, name, value):
"""
Ensure that C{value} is a valid C{name} attribute on C{resource_type}.
Does this by finding the representation for the default, canonical GET
method (as opposed to the many "named" GET methods that exist.)
@param resource_type: The resource type to check the attribute
against.
@param name: The name of the attribute.
@param value: The value to check.
"""
xml_id = self._find_representation_id(resource_type, 'get')
self._check_attribute_representation(xml_id, name, value)
def _check_attribute_representation(self, xml_id, name, value):
"""
Ensure that C{value} is a valid value for C{name} with the
representation definition matching C{xml_id}.
@param xml_id: The XML ID for the representation to check the
attribute against.
@param name: The name of the attribute.
@param value: The value to check.
@raises IntegrityError: Raised if C{name} is not a valid attribute
name or if C{value}'s type is not valid for the attribute.
"""
representation = self._application.representation_definitions[xml_id]
parameters = dict((child.get("name"), child)
for child in representation.tag)
if name not in parameters:
raise IntegrityError("%s not found" % name)
parameter = parameters[name]
data_type = parameter.get("type")
if data_type is None:
if not isinstance(value, basestring):
raise IntegrityError(
"%s is not a str or unicode for %s" % (value, name))
elif data_type == "xsd:dateTime":
if not isinstance(value, datetime):
raise IntegrityError(
"%s is not a datetime for %s" % (value, name))
def _get_method(self, resource_type, name):
"""Get the C{name} method on C{resource_type}.
@param resource_type: The method's resource type.
@param name: The name of the method.
@raises IntegrityError: Raised if a method called C{name} is not
available on C{resource_type}.
@return: The XML element for the method from the WADL.
"""
if name in self.special_methods:
return
resource_name = resource_type.tag.get("id")
xml_id = "%s-%s" % (resource_name, name)
try:
[get_method] = find_by_attribute(resource_type.tag, 'id', xml_id)
except ValueError:
raise IntegrityError(
"%s is not a method of %s" % (name, resource_name))
return get_method
def _run_method(self, name, method, *args, **kwargs):
"""Run a method and convert its result into a L{FakeResource}.
If the result represents an object it is validated against the WADL
definition before being returned.
@param name: The name of the method.
@param method: A callable.
@param args: Arguments to pass to the callable.
@param kwargs: Keyword arguments to pass to the callable.
@return: A L{FakeResource} representing the result if it's an object.
@raises IntegrityError: Raised if the return value from the method
isn't valid.
"""
result = method(*args, **kwargs)
if name in self.special_methods:
return result
else:
return self._create_resource(self._resource_type, name, result)
def _create_resource(self, resource_type, name, result):
"""Create a new L{FakeResource} for C{resource_type} method call result.
@param resource_type: The resource type of the method.
@param name: The name of the method on C{resource_type}.
@param result: The result of calling the method.
@raises IntegrityError: Raised if C{result} is an invalid return value
for the method.
@return: A L{FakeResource} for C{result}.
"""
resource_name = resource_type.tag.get("id")
if resource_name == name:
name = "get"
xml_id = self._find_representation_id(resource_type, name)
xml_id = strip_suffix(xml_id, '-full')
if xml_id not in self._application.resource_types:
xml_id += '-resource'
result_resource_type = self._application.resource_types[xml_id]
self._check_resource_type(result_resource_type, result)
# XXX: Should this wrap in collection?
return FakeResource(self._application, result_resource_type, result)
def _get_child_resource_type(self, resource_type):
"""Get the name and resource type for the entries in a collection.
@param resource_type: The resource type for a collection.
@return: (name, resource_type), where 'name' is the name of the child
resource type and 'resource_type' is the corresponding resource
type.
"""
xml_id = self._find_representation_id(resource_type, 'get')
representation_definition = (
self._application.representation_definitions[xml_id])
[entry_links] = find_by_attribute(
representation_definition.tag, 'name', 'entry_links')
[resource_type] = list(entry_links)
resource_type_url = resource_type.get("resource_type")
resource_type_name = resource_type_url.split("#")[1]
return (
resource_type_name,
self._application.get_resource_type(resource_type_url))
def _check_entries(self, resource_type, entries):
"""Ensure that C{entries} are valid for a C{resource_type} collection.
@param resource_type: The resource type of the collection the entries
are in.
@param entries: A list of dicts representing objects in the
collection.
@return: (name, resource_type), where 'name' is the name of the child
resource type and 'resource_type' is the corresponding resource
type.
"""
name, child_resource_type = self._get_child_resource_type(resource_type)
for entry in entries:
self._check_resource_type(child_resource_type, entry)
return name, child_resource_type
def __repr__(self):
"""
The resource type, identifier if available, and memory address are
used to generate a representation of this fake resource.
"""
name = self._resource_type.tag.get("id")
key = "object"
key = self._values.get("id", key)
key = self._values.get("name", key)
return "<%s %s %s at %s>" % (
self.__class__.__name__, name, key, hex(id(self)))
class FakeRoot(FakeResource):
"""Fake root object for an application."""
def __init__(self, application):
"""Create a L{FakeResource} for the service root of C{application}.
@param application: A C{wadllib.application.Application} instance.
"""
resource_type = application.get_resource_type(
application.markup_url + "#service-root")
super(FakeRoot, self).__init__(application, resource_type)
class FakeEntry(FakeResource):
"""A fake resource for an entry."""
class FakeCollection(FakeResource):
"""A fake resource for a collection."""
def __init__(self, application, resource_type, values=None,
name=None, child_resource_type=None):
super(FakeCollection, self).__init__(application, resource_type, values)
self.__dict__.update({"_name": name,
"_child_resource_type": child_resource_type})
def __iter__(self):
"""Iterate items if this resource has an C{entries} attribute."""
entries = self._values.get("entries", ())
for entry in entries:
yield self._create_resource(self._child_resource_type, self._name,
entry)
def __getitem__(self, key):
"""Look up a slice, or a subordinate resource by index.
@param key: An individual object key or a C{slice}.
@raises IndexError: Raised if an invalid key is provided.
@return: A L{FakeResource} instance for the entry matching C{key}.
"""
entries = list(self)
if isinstance(key, slice):
start = key.start or 0
stop = key.stop
if start < 0:
raise ValueError("Collection slices must have a nonnegative "
"start point.")
if stop < 0:
raise ValueError("Collection slices must have a definite, "
"nonnegative end point.")
return entries.__getitem__(key)
elif isinstance(key, int):
return entries.__getitem__(key)
else:
raise IndexError("Do not support index lookups yet.")
launchpadlib-1.10.2/src/launchpadlib/tests/ 0000755 0000000 0000000 00000000000 11775571157 017316 5 ustar root root launchpadlib-1.10.2/src/launchpadlib/tests/__init__.py 0000644 0000000 0000000 00000001302 11775055270 021414 0 ustar root root # Copyright 2008 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Tests for launchpadlib"""
launchpadlib-1.10.2/src/launchpadlib/tests/test_http.py 0000644 0000000 0000000 00000022067 11775055270 021706 0 ustar root root # Copyright 2010 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Tests for the LaunchpadOAuthAwareHTTP class."""
from collections import deque
import tempfile
import unittest
from simplejson import dumps, JSONDecodeError
from launchpadlib.errors import Unauthorized
from launchpadlib.credentials import UnencryptedFileCredentialStore
from launchpadlib.launchpad import (
Launchpad,
LaunchpadOAuthAwareHttp,
)
from launchpadlib.testing.helpers import (
NoNetworkAuthorizationEngine,
NoNetworkLaunchpad,
)
# The simplest WADL that looks like a representation of the service root.
SIMPLE_WADL = '''
'''
# The simplest JSON that looks like a representation of the service root.
SIMPLE_JSON = dumps({})
class Response:
"""A fake HTTP response object."""
def __init__(self, status, content):
self.status = status
self.content = content
class SimulatedResponsesHttp(LaunchpadOAuthAwareHttp):
"""Responds to HTTP requests by shifting responses off a stack."""
def __init__(self, responses, *args):
"""Constructor.
:param responses: A list of HttpResponse objects to use
in response to requests.
"""
super(SimulatedResponsesHttp, self).__init__(*args)
self.sent_responses = []
self.unsent_responses = responses
self.cache = None
def _request(self, *args):
response = self.unsent_responses.popleft()
self.sent_responses.append(response)
return self.retry_on_bad_token(response, response.content, *args)
class SimulatedResponsesLaunchpad(Launchpad):
# Every Http object generated by this class will return these
# responses, in order.
responses = []
def httpFactory(self, *args):
return SimulatedResponsesHttp(
deque(self.responses), self, self.authorization_engine, *args)
@classmethod
def credential_store_factory(cls, credential_save_failed):
return UnencryptedFileCredentialStore(
tempfile.mkstemp()[1], credential_save_failed)
class SimulatedResponsesTestCase(unittest.TestCase):
"""Test cases that give fake responses to launchpad's HTTP requests."""
def setUp(self):
"""Clear out the list of simulated responses."""
SimulatedResponsesLaunchpad.responses = []
self.engine = NoNetworkAuthorizationEngine(
'http://api.example.com/', 'application name')
def launchpad_with_responses(self, *responses):
"""Use simulated HTTP responses to get a Launchpad object.
The given Response objects will be sent, in order, in response
to launchpadlib's requests.
:param responses: Some number of Response objects.
:return: The Launchpad object, assuming that errors in the
simulated requests didn't prevent one from being created.
"""
SimulatedResponsesLaunchpad.responses = responses
return SimulatedResponsesLaunchpad.login_with(
'application name', authorization_engine=self.engine)
class TestAbilityToParseData(SimulatedResponsesTestCase):
"""Test launchpadlib's ability to handle the sample data.
To create a Launchpad object, two HTTP requests must succeed and
return usable data: the requests for the WADL and JSON
representations of the service root. This test shows that the
minimal data in SIMPLE_WADL and SIMPLE_JSON is good enough to
create a Launchpad object.
"""
def test_minimal_data(self):
"""Make sure that launchpadlib can use the minimal data."""
launchpad = self.launchpad_with_responses(
Response(200, SIMPLE_WADL),
Response(200, SIMPLE_JSON))
def test_bad_wadl(self):
"""Show that bad WADL causes an exception."""
self.assertRaises(
SyntaxError, self.launchpad_with_responses,
Response(200, "This is not WADL."),
Response(200, SIMPLE_JSON))
def test_bad_json(self):
"""Show that bad JSON causes an exception."""
self.assertRaises(
JSONDecodeError, self.launchpad_with_responses,
Response(200, SIMPLE_WADL),
Response(200, "This is not JSON."))
class TestTokenFailureDuringRequest(SimulatedResponsesTestCase):
"""Test access token failures during a request.
launchpadlib makes two HTTP requests on startup, to get the WADL
and JSON representations of the service root. If Launchpad
receives a 401 error during this process, it will acquire a fresh
access token and try again.
"""
def test_good_token(self):
"""If our token is good, we never get another one."""
SimulatedResponsesLaunchpad.responses = [
Response(200, SIMPLE_WADL),
Response(200, SIMPLE_JSON)]
self.assertEquals(self.engine.access_tokens_obtained, 0)
launchpad = SimulatedResponsesLaunchpad.login_with(
'application name', authorization_engine=self.engine)
self.assertEquals(self.engine.access_tokens_obtained, 1)
def test_bad_token(self):
"""If our token is bad, we get another one."""
SimulatedResponsesLaunchpad.responses = [
Response(401, "Invalid token."),
Response(200, SIMPLE_WADL),
Response(200, SIMPLE_JSON)]
self.assertEquals(self.engine.access_tokens_obtained, 0)
launchpad = SimulatedResponsesLaunchpad.login_with(
'application name', authorization_engine=self.engine)
self.assertEquals(self.engine.access_tokens_obtained, 2)
def test_expired_token(self):
"""If our token is expired, we get another one."""
SimulatedResponsesLaunchpad.responses = [
Response(401, "Expired token."),
Response(200, SIMPLE_WADL),
Response(200, SIMPLE_JSON)]
self.assertEquals(self.engine.access_tokens_obtained, 0)
launchpad = SimulatedResponsesLaunchpad.login_with(
'application name', authorization_engine=self.engine)
self.assertEquals(self.engine.access_tokens_obtained, 2)
def test_unknown_token(self):
"""If our token is unknown, we get another one."""
SimulatedResponsesLaunchpad.responses = [
Response(401, "Unknown access token."),
Response(200, SIMPLE_WADL),
Response(200, SIMPLE_JSON)]
self.assertEquals(self.engine.access_tokens_obtained, 0)
launchpad = SimulatedResponsesLaunchpad.login_with(
'application name', authorization_engine=self.engine)
self.assertEquals(self.engine.access_tokens_obtained, 2)
def test_delayed_error(self):
"""We get another token no matter when the error happens."""
SimulatedResponsesLaunchpad.responses = [
Response(200, SIMPLE_WADL),
Response(401, "Expired token."),
Response(200, SIMPLE_JSON)]
self.assertEquals(self.engine.access_tokens_obtained, 0)
launchpad = SimulatedResponsesLaunchpad.login_with(
'application name', authorization_engine=self.engine)
self.assertEquals(self.engine.access_tokens_obtained, 2)
def test_many_errors(self):
"""We'll keep getting new tokens as long as tokens are the problem."""
SimulatedResponsesLaunchpad.responses = [
Response(401, "Invalid token."),
Response(200, SIMPLE_WADL),
Response(401, "Expired token."),
Response(401, "Invalid token."),
Response(200, SIMPLE_JSON)]
self.assertEquals(self.engine.access_tokens_obtained, 0)
launchpad = SimulatedResponsesLaunchpad.login_with(
'application name', authorization_engine=self.engine)
self.assertEquals(self.engine.access_tokens_obtained, 4)
def test_other_unauthorized(self):
"""If the token is not at fault, a 401 error raises an exception."""
SimulatedResponsesLaunchpad.responses = [
Response(401, "Some other error.")]
self.assertRaises(
Unauthorized, SimulatedResponsesLaunchpad.login_with,
'application name', authorization_engine=self.engine)
launchpadlib-1.10.2/src/launchpadlib/tests/test_credential_store.py 0000644 0000000 0000000 00000017011 11775055270 024246 0 ustar root root # Copyright 2010-2011 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Tests for the credential store classes."""
import os
import tempfile
import unittest
from base64 import b64decode
from launchpadlib.testing.helpers import (
fake_keyring,
InMemoryKeyring,
)
from launchpadlib.credentials import (
AccessToken,
Credentials,
KeyringCredentialStore,
UnencryptedFileCredentialStore,
)
class CredentialStoreTestCase(unittest.TestCase):
def make_credential(self, consumer_key):
"""Helper method to make a fake credential."""
return Credentials(
"app name", consumer_secret='consumer_secret:42',
access_token=AccessToken(consumer_key, 'access_secret:168'))
class TestUnencryptedFileCredentialStore(CredentialStoreTestCase):
"""Tests for the UnencryptedFileCredentialStore class."""
def setUp(self):
ignore, self.filename = tempfile.mkstemp()
self.store = UnencryptedFileCredentialStore(self.filename)
def tearDown(self):
if os.path.exists(self.filename):
os.remove(self.filename)
def test_save_and_load(self):
# Make sure you can save and load credentials to a file.
credential = self.make_credential("consumer key")
self.store.save(credential, "unique key")
credential2 = self.store.load("unique key")
self.assertEquals(credential.consumer.key, credential2.consumer.key)
def test_unique_id_doesnt_matter(self):
# If a file contains a credential, that credential will be
# accessed no matter what unique ID you specify.
credential = self.make_credential("consumer key")
self.store.save(credential, "some key")
credential2 = self.store.load("some other key")
self.assertEquals(credential.consumer.key, credential2.consumer.key)
def test_file_only_contains_one_credential(self):
# A credential file may contain only one credential. If you
# write two credentials with different unique IDs to the same
# file, the first credential will be overwritten with the
# second.
credential1 = self.make_credential("consumer key")
credential2 = self.make_credential("consumer key2")
self.store.save(credential1, "unique key 1")
self.store.save(credential1, "unique key 2")
loaded = self.store.load("unique key 1")
self.assertEquals(loaded.consumer.key, credential2.consumer.key)
class TestKeyringCredentialStore(CredentialStoreTestCase):
"""Tests for the KeyringCredentialStore class."""
def setUp(self):
self.keyring = InMemoryKeyring()
self.store = KeyringCredentialStore()
def test_save_and_load(self):
# Make sure you can save and load credentials to a keyring.
with fake_keyring(self.keyring):
credential = self.make_credential("consumer key")
self.store.save(credential, "unique key")
credential2 = self.store.load("unique key")
self.assertEquals(
credential.consumer.key, credential2.consumer.key)
def test_lookup_by_unique_key(self):
# Credentials in the keyring are looked up by the unique ID
# under which they were stored.
with fake_keyring(self.keyring):
credential1 = self.make_credential("consumer key1")
self.store.save(credential1, "key 1")
credential2 = self.make_credential("consumer key2")
self.store.save(credential2, "key 2")
loaded1 = self.store.load("key 1")
self.assertEquals(
credential1.consumer.key, loaded1.consumer.key)
loaded2 = self.store.load("key 2")
self.assertEquals(
credential2.consumer.key, loaded2.consumer.key)
def test_reused_unique_id_overwrites_old_credential(self):
# Writing a credential to the keyring with a given unique ID
# will overwrite any credential stored under that ID.
with fake_keyring(self.keyring):
credential1 = self.make_credential("consumer key1")
self.store.save(credential1, "the only key")
credential2 = self.make_credential("consumer key2")
self.store.save(credential2, "the only key")
loaded = self.store.load("the only key")
self.assertEquals(
credential2.consumer.key, loaded.consumer.key)
def test_bad_unique_id_returns_none(self):
# Trying to load a credential without providing a good unique
# ID will get you None.
with fake_keyring(self.keyring):
self.assertEquals(None, self.store.load("no such key"))
def test_keyring_returns_unicode(self):
# Kwallet is reported to sometimes return Unicode, which broke the
# credentials parsing. This test ensures a Unicode password is
# handled correctly. (See bug lp:877374)
class UnicodeInMemoryKeyring(InMemoryKeyring):
def get_password(self, service, username):
return unicode(
super(UnicodeInMemoryKeyring, self).get_password(
service, username))
self.keyring = UnicodeInMemoryKeyring()
with fake_keyring(self.keyring):
credential = self.make_credential("consumer key")
self.store.save(credential, "unique key")
credential2 = self.store.load("unique key")
self.assertEquals(
credential.consumer.key, credential2.consumer.key)
self.assertEquals(
credential.consumer.secret, credential2.consumer.secret)
def test_nonencoded_key_handled(self):
# For backwards compatibility with keys that are not base 64 encoded.
class UnencodedInMemoryKeyring(InMemoryKeyring):
def get_password(self, service, username):
pw = super(UnencodedInMemoryKeyring, self).get_password(
service, username)
return b64decode(pw[5:])
self.keyring = UnencodedInMemoryKeyring()
with fake_keyring(self.keyring):
credential = self.make_credential("consumer key")
self.store.save(credential, "unique key")
credential2 = self.store.load("unique key")
self.assertEquals(
credential.consumer.key, credential2.consumer.key)
self.assertEquals(
credential.consumer.secret, credential2.consumer.secret)
def test_corrupted_key_handled(self):
# A corrupted password results in None being returned.
class CorruptedInMemoryKeyring(InMemoryKeyring):
def get_password(self, service, username):
return "bad"
self.keyring = CorruptedInMemoryKeyring()
with fake_keyring(self.keyring):
credential = self.make_credential("consumer key")
self.store.save(credential, "unique key")
credential2 = self.store.load("unique key")
self.assertIsNone(credential2)
launchpadlib-1.10.2/src/launchpadlib/tests/test_launchpad.py 0000644 0000000 0000000 00000074034 11775055270 022667 0 ustar root root # Copyright 2009, 2011 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Tests for the Launchpad class."""
__metaclass__ = type
from contextlib import contextmanager
import os
import shutil
import socket
import stat
import tempfile
import unittest
import warnings
from lazr.restfulclient.resource import ServiceRoot
from launchpadlib.credentials import (
AccessToken,
Credentials,
)
from launchpadlib import uris
import launchpadlib.launchpad
from launchpadlib.launchpad import Launchpad
from launchpadlib.testing.helpers import (
assert_keyring_not_imported,
BadSaveKeyring,
fake_keyring,
FauxSocketModule,
InMemoryKeyring,
NoNetworkAuthorizationEngine,
NoNetworkLaunchpad,
)
from launchpadlib.credentials import (
KeyringCredentialStore,
)
# A dummy service root for use in tests
SERVICE_ROOT = "http://api.example.com/"
class TestResourceTypeClasses(unittest.TestCase):
"""launchpadlib must know about restfulclient's resource types."""
def test_resource_types(self):
# Make sure that Launchpad knows about every special resource
# class defined by lazr.restfulclient.
for name, cls in ServiceRoot.RESOURCE_TYPE_CLASSES.items():
self.assertEqual(Launchpad.RESOURCE_TYPE_CLASSES[name], cls)
class TestNameLookups(unittest.TestCase):
"""Test the utility functions in the 'uris' module."""
def setUp(self):
self.aliases = sorted(
['production', 'qastaging', 'staging', 'dogfood', 'dev',
'test_dev', 'edge'])
@contextmanager
def edge_deprecation_error(self):
# Run some code and assert that a deprecation error was issued
# due to attempted access to the edge server.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
yield
self.assertEqual(len(caught), 1)
warning, = caught
self.assertTrue(issubclass(warning.category, DeprecationWarning))
self.assertTrue("no longer exists" in warning.message.message)
def test_short_names(self):
# Ensure the short service names are all supported.
self.assertEqual(sorted(uris.service_roots.keys()), self.aliases)
self.assertEqual(sorted(uris.web_roots.keys()), self.aliases)
def test_edge_service_root_is_production(self):
# The edge server no longer exists, so if the client wants
# edge we give them production.
with self.edge_deprecation_error():
self.assertEqual(uris.lookup_service_root('edge'),
uris.lookup_service_root('production'))
def test_edge_service_root_is_production(self):
# The edge server no longer exists, so if the client wants
# edge we give them production.
with self.edge_deprecation_error():
self.assertEqual(uris.lookup_web_root('edge'),
uris.lookup_web_root('production'))
def test_edge_service_root_url_becomes_production(self):
with self.edge_deprecation_error():
self.assertEqual(uris.lookup_service_root(uris.EDGE_SERVICE_ROOT),
uris.lookup_service_root('production'))
def test_edge_web_root_url_becomes_production(self):
with self.edge_deprecation_error():
self.assertEqual(uris.lookup_web_root(uris.EDGE_WEB_ROOT),
uris.lookup_web_root('production'))
def test_top_level_edge_constant_becomes_production(self):
with self.edge_deprecation_error():
self.assertEqual(uris.lookup_service_root(uris.EDGE_SERVICE_ROOT),
uris.lookup_service_root('production'))
def test_edge_server_equivalent_string_becomes_production(self):
with self.edge_deprecation_error():
self.assertEqual(
uris.lookup_service_root('https://api.edge.launchpad.net/'),
uris.lookup_service_root('production'))
def test_edge_web_server_equivalent_string_becomes_production(self):
with self.edge_deprecation_error():
self.assertEqual(
uris.lookup_web_root('https://edge.launchpad.net/'),
uris.lookup_web_root('production'))
def test_lookups(self):
"""Ensure that short service names turn into long service names."""
# If the service name is a known alias, lookup methods convert
# it to a URL.
with self.edge_deprecation_error():
for alias in self.aliases:
self.assertEqual(
uris.lookup_service_root(alias), uris.service_roots[alias])
with self.edge_deprecation_error():
for alias in self.aliases:
self.assertEqual(
uris.lookup_web_root(alias), uris.web_roots[alias])
# If the service name is a valid URL, lookup methods let it
# through.
other_root = "http://some-other-server.com"
self.assertEqual(uris.lookup_service_root(other_root), other_root)
self.assertEqual(uris.lookup_web_root(other_root), other_root)
# Otherwise, lookup methods raise an exception.
not_a_url = "not-a-url"
self.assertRaises(ValueError, uris.lookup_service_root, not_a_url)
self.assertRaises(ValueError, uris.lookup_web_root, not_a_url)
class TestServiceNameWithEmbeddedVersion(unittest.TestCase):
"""Reject service roots that include the version at the end of the URL.
If the service root is "http://api.launchpad.net/beta/" and the
version is "beta", the launchpadlib constructor will raise an
exception.
This happens with scripts that were written against old versions
of launchpadlib. The alternative is to try to silently fix it (the
fix will eventually break as new versions of the web service are
released) or to go ahead and make a request to
http://api.launchpad.net/beta/beta/, and cause an unhelpful 404
error.
"""
def test_service_name_with_embedded_version(self):
# Basic test. If there were no exception raised here,
# launchpadlib would make a request to
# /version-foo/version-foo.
version = "version-foo"
root = uris.service_roots['staging'] + version
try:
Launchpad(None, None, None, service_root=root, version=version)
except ValueError, e:
self.assertTrue(str(e).startswith(
"It looks like you're using a service root that incorporates "
'the name of the web service version ("version-foo")'))
else:
raise AssertionError(
"Expected a ValueError that was not thrown!")
# Make sure the problematic URL is caught even if it has a
# slash on the end.
root += '/'
self.assertRaises(ValueError, Launchpad, None, None, None,
service_root=root, version=version)
# Test that the default version has the same problem
# when no explicit version is specified
default_version = NoNetworkLaunchpad.DEFAULT_VERSION
root = uris.service_roots['staging'] + default_version + '/'
self.assertRaises(ValueError, Launchpad, None, None, None,
service_root=root)
class TestRequestTokenAuthorizationEngine(unittest.TestCase):
"""Tests for the RequestTokenAuthorizationEngine class."""
def test_app_must_be_identified(self):
self.assertRaises(
ValueError, NoNetworkAuthorizationEngine, SERVICE_ROOT)
def test_application_name_identifies_app(self):
NoNetworkAuthorizationEngine(SERVICE_ROOT, application_name='name')
def test_consumer_name_identifies_app(self):
NoNetworkAuthorizationEngine(SERVICE_ROOT, consumer_name='name')
def test_conflicting_app_identification(self):
# You can't specify both application_name and consumer_name.
self.assertRaises(
ValueError, NoNetworkAuthorizationEngine,
SERVICE_ROOT, application_name='name1', consumer_name='name2')
# This holds true even if you specify the same value for
# both. They're not the same thing.
self.assertRaises(
ValueError, NoNetworkAuthorizationEngine,
SERVICE_ROOT, application_name='name', consumer_name='name')
class TestLaunchpadLoginWithCredentialsFile(unittest.TestCase):
"""Tests for Launchpad.login_with() with a credentials file."""
def test_filename(self):
ignore, filename = tempfile.mkstemp()
launchpad = NoNetworkLaunchpad.login_with(
application_name='not important', credentials_file=filename)
# The credentials are stored unencrypted in the file you
# specify.
credentials = Credentials.load_from_path(filename)
self.assertEquals(credentials.consumer.key,
launchpad.credentials.consumer.key)
os.remove(filename)
def test_cannot_specify_both_filename_and_store(self):
ignore, filename = tempfile.mkstemp()
store = KeyringCredentialStore()
self.assertRaises(
ValueError, NoNetworkLaunchpad.login_with,
application_name='not important', credentials_file=filename,
credential_store=store)
os.remove(filename)
class KeyringTest(unittest.TestCase):
"""Base class for tests that use the keyring."""
def setUp(self):
# The real keyring package should never be imported during tests.
assert_keyring_not_imported()
# For these tests we want to use a dummy keyring implementation
# that only stores data in memory.
launchpadlib.credentials.keyring = InMemoryKeyring()
def tearDown(self):
# Remove the fake keyring module we injected during setUp.
del launchpadlib.credentials.keyring
class TestLaunchpadLoginWith(KeyringTest):
"""Tests for Launchpad.login_with()."""
def setUp(self):
super(TestLaunchpadLoginWith, self).setUp()
self.temp_dir = tempfile.mkdtemp()
def tearDown(self):
super(TestLaunchpadLoginWith, self).tearDown()
shutil.rmtree(self.temp_dir)
def test_dirs_created(self):
# The path we pass into login_with() is the directory where
# cache for all service roots are stored.
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
NoNetworkLaunchpad.login_with(
'not important', service_root=SERVICE_ROOT,
launchpadlib_dir=launchpadlib_dir)
# The 'launchpadlib' dir got created.
self.assertTrue(os.path.isdir(launchpadlib_dir))
# A directory for the passed in service root was created.
service_path = os.path.join(launchpadlib_dir, 'api.example.com')
self.assertTrue(os.path.isdir(service_path))
# Inside the service root directory, there is a 'cache'
# directory.
self.assertTrue(
os.path.isdir(os.path.join(service_path, 'cache')))
# In older versions there was also a 'credentials' directory,
# but no longer.
credentials_path = os.path.join(service_path, 'credentials')
self.assertFalse(os.path.isdir(credentials_path))
def test_dirs_created_are_changed_to_secure(self):
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
# Verify a newly created-by-hand directory is insecure
os.mkdir(launchpadlib_dir)
os.chmod(launchpadlib_dir, 0755)
self.assertTrue(os.path.isdir(launchpadlib_dir))
statinfo = os.stat(launchpadlib_dir)
mode = stat.S_IMODE(statinfo.st_mode)
self.assertNotEqual(mode, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
NoNetworkLaunchpad.login_with(
'not important', service_root=SERVICE_ROOT,
launchpadlib_dir=launchpadlib_dir)
# Verify the mode has been changed to 0700
statinfo = os.stat(launchpadlib_dir)
mode = stat.S_IMODE(statinfo.st_mode)
self.assertEqual(mode, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
def test_dirs_created_are_secure(self):
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
NoNetworkLaunchpad.login_with(
'not important', service_root=SERVICE_ROOT,
launchpadlib_dir=launchpadlib_dir)
self.assertTrue(os.path.isdir(launchpadlib_dir))
# Verify the mode is safe
statinfo = os.stat(launchpadlib_dir)
mode = stat.S_IMODE(statinfo.st_mode)
self.assertEqual(mode, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
def test_version_is_propagated(self):
# Make sure the login_with() method conveys the 'version'
# argument all the way to the Launchpad object. The
# credentials will be cached to disk.
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
launchpad = NoNetworkLaunchpad.login_with(
'not important', service_root=SERVICE_ROOT,
launchpadlib_dir=launchpadlib_dir, version="foo")
self.assertEquals(launchpad.passed_in_args['version'], 'foo')
# Now execute the same test a second time. This time, the
# credentials are loaded from disk and a different code path
# is executed. We want to make sure this code path propagates
# the 'version' argument.
launchpad = NoNetworkLaunchpad.login_with(
'not important', service_root=SERVICE_ROOT,
launchpadlib_dir=launchpadlib_dir, version="bar")
self.assertEquals(launchpad.passed_in_args['version'], 'bar')
def test_application_name_is_propagated(self):
# Create a Launchpad instance for a given application name.
# Credentials are stored, but they don't include the
# application name, since multiple applications may share a
# single system-wide credential.
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
launchpad = NoNetworkLaunchpad.login_with(
'very important', service_root=SERVICE_ROOT,
launchpadlib_dir=launchpadlib_dir)
self.assertEquals(
launchpad.credentials.consumer.application_name, 'very important')
# Now execute the same test a second time. This time, the
# credentials are loaded from disk and a different code path
# is executed. We want to make sure this code path propagates
# the application name, instead of picking an empty one from
# disk.
launchpad = NoNetworkLaunchpad.login_with(
'very important', service_root=SERVICE_ROOT,
launchpadlib_dir=launchpadlib_dir)
self.assertEquals(
launchpad.credentials.consumer.application_name, 'very important')
def test_authorization_engine_is_propagated(self):
# You can pass in a custom authorization engine, which will be
# used to get a request token and exchange it for an access
# token.
engine = NoNetworkAuthorizationEngine(
SERVICE_ROOT, 'application name')
NoNetworkLaunchpad.login_with(authorization_engine=engine)
self.assertEquals(engine.request_tokens_obtained, 1)
self.assertEquals(engine.access_tokens_obtained, 1)
def test_login_with_must_identify_application(self):
# If you call login_with without identifying your application
# you'll get an error.
self.assertRaises(ValueError, NoNetworkLaunchpad.login_with)
def test_application_name_identifies_app(self):
# If you pass in application_name, that's good enough to identify
# your application.
NoNetworkLaunchpad.login_with(application_name="name")
def test_consumer_name_identifies_app(self):
# If you pass in consumer_name, that's good enough to identify
# your application.
NoNetworkLaunchpad.login_with(consumer_name="name")
def test_inconsistent_application_name_rejected(self):
"""Catch an attempt to specify inconsistent application_names."""
engine = NoNetworkAuthorizationEngine(
SERVICE_ROOT, 'application name1')
self.assertRaises(ValueError, NoNetworkLaunchpad.login_with,
"application name2",
authorization_engine=engine)
def test_inconsistent_consumer_name_rejected(self):
"""Catch an attempt to specify inconsistent application_names."""
engine = NoNetworkAuthorizationEngine(
SERVICE_ROOT, None, consumer_name="consumer_name1")
self.assertRaises(ValueError, NoNetworkLaunchpad.login_with,
"consumer_name2",
authorization_engine=engine)
def test_inconsistent_allow_access_levels_rejected(self):
"""Catch an attempt to specify inconsistent allow_access_levels."""
engine = NoNetworkAuthorizationEngine(
SERVICE_ROOT, consumer_name="consumer",
allow_access_levels=['FOO'])
self.assertRaises(ValueError, NoNetworkLaunchpad.login_with,
None, consumer_name="consumer",
allow_access_levels=['BAR'],
authorization_engine=engine)
def test_inconsistent_credential_save_failed(self):
# Catch an attempt to specify inconsistent callbacks for
# credential save failure.
def callback1():
pass
store = KeyringCredentialStore(credential_save_failed=callback1)
def callback2():
pass
self.assertRaises(ValueError, NoNetworkLaunchpad.login_with,
"app name", credential_store=store,
credential_save_failed=callback2)
def test_non_desktop_integration(self):
# When doing a non-desktop integration, you must specify a
# consumer_name. You can pass a list of allowable access
# levels into login_with().
launchpad = NoNetworkLaunchpad.login_with(
consumer_name="consumer", allow_access_levels=['FOO'])
self.assertEquals(launchpad.credentials.consumer.key, "consumer")
self.assertEquals(launchpad.credentials.consumer.application_name,
None)
self.assertEquals(launchpad.authorization_engine.allow_access_levels,
['FOO'])
def test_desktop_integration_doesnt_happen_without_consumer_name(self):
# The only way to do a non-desktop integration is to specify a
# consumer_name. If you specify application_name instead, your
# value for allow_access_levels is ignored, and a desktop
# integration is performed.
launchpad = NoNetworkLaunchpad.login_with(
'application name', allow_access_levels=['FOO'])
self.assertEquals(launchpad.authorization_engine.allow_access_levels,
['DESKTOP_INTEGRATION'])
def test_no_credentials_creates_new_credential(self):
# If no credentials are found, a desktop-wide credential is created.
timeout = object()
proxy_info = object()
launchpad = NoNetworkLaunchpad.login_with(
'app name', launchpadlib_dir=self.temp_dir,
service_root=SERVICE_ROOT, timeout=timeout, proxy_info=proxy_info)
# Here's the new credential.
self.assertEqual(launchpad.credentials.access_token.key,
NoNetworkAuthorizationEngine.ACCESS_TOKEN_KEY)
self.assertEqual(launchpad.credentials.consumer.application_name,
'app name')
self.assertEquals(launchpad.authorization_engine.allow_access_levels,
['DESKTOP_INTEGRATION'])
# The expected arguments were passed in to the Launchpad
# constructor.
expected_arguments = dict(
service_root=SERVICE_ROOT,
cache=os.path.join(self.temp_dir, 'api.example.com', 'cache'),
timeout=timeout,
proxy_info=proxy_info,
version=NoNetworkLaunchpad.DEFAULT_VERSION)
self.assertEqual(launchpad.passed_in_args, expected_arguments)
def test_anonymous_login(self):
"""Test the anonymous login helper function."""
launchpad = NoNetworkLaunchpad.login_anonymously(
'anonymous access', launchpadlib_dir=self.temp_dir,
service_root=SERVICE_ROOT)
self.assertEqual(launchpad.credentials.access_token.key, '')
self.assertEqual(launchpad.credentials.access_token.secret, '')
# Test that anonymous credentials are not saved.
credentials_path = os.path.join(
self.temp_dir, 'api.example.com', 'credentials',
'anonymous access')
self.assertFalse(os.path.exists(credentials_path))
def test_existing_credentials_arguments_passed_on(self):
# When re-using existing credentials, the arguments login_with
# is called with are passed on the the __init__() method.
os.makedirs(
os.path.join(self.temp_dir, 'api.example.com', 'credentials'))
credentials_file_path = os.path.join(
self.temp_dir, 'api.example.com', 'credentials', 'app name')
credentials = Credentials(
'app name', consumer_secret='consumer_secret:42',
access_token=AccessToken('access_key:84', 'access_secret:168'))
credentials.save_to_path(credentials_file_path)
timeout = object()
proxy_info = object()
version = "foo"
launchpad = NoNetworkLaunchpad.login_with(
'app name', launchpadlib_dir=self.temp_dir,
service_root=SERVICE_ROOT, timeout=timeout, proxy_info=proxy_info,
version=version)
expected_arguments = dict(
service_root=SERVICE_ROOT,
timeout=timeout,
proxy_info=proxy_info,
version=version,
cache=os.path.join(self.temp_dir, 'api.example.com', 'cache'))
for key, expected in expected_arguments.items():
actual = launchpad.passed_in_args[key]
self.assertEqual(actual, expected)
def test_None_launchpadlib_dir(self):
# If no launchpadlib_dir is passed in to login_with,
# $HOME/.launchpadlib is used.
old_home = os.environ['HOME']
os.environ['HOME'] = self.temp_dir
launchpad = NoNetworkLaunchpad.login_with(
'app name', service_root=SERVICE_ROOT)
# Reset the environment to the old value.
os.environ['HOME'] = old_home
cache_dir = launchpad.passed_in_args['cache']
launchpadlib_dir = os.path.abspath(
os.path.join(cache_dir, '..', '..'))
self.assertEqual(
launchpadlib_dir, os.path.join(self.temp_dir, '.launchpadlib'))
self.assertTrue(os.path.exists(
os.path.join(launchpadlib_dir, 'api.example.com', 'cache')))
def test_short_service_name(self):
# A short service name is converted to the full service root URL.
launchpad = NoNetworkLaunchpad.login_with('app name', 'staging')
self.assertEqual(
launchpad.passed_in_args['service_root'],
'https://api.staging.launchpad.net/')
# A full URL as the service name is left alone.
launchpad = NoNetworkLaunchpad.login_with(
'app name', uris.service_roots['staging'])
self.assertEqual(
launchpad.passed_in_args['service_root'],
uris.service_roots['staging'])
# A short service name that does not match one of the
# pre-defined service root names, and is not a valid URL,
# raises an exception.
launchpad = ('app name', 'https://')
self.assertRaises(
ValueError, NoNetworkLaunchpad.login_with, 'app name', 'foo')
def test_max_failed_attempts_accepted(self):
# You can pass in a value for the 'max_failed_attempts'
# argument, even though that argument doesn't do anything.
NoNetworkLaunchpad.login_with(
'not important', max_failed_attempts=5)
class TestDeprecatedLoginMethods(KeyringTest):
"""Make sure the deprecated login methods still work."""
def test_login_is_deprecated(self):
# login() works but triggers a deprecation warning.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
NoNetworkLaunchpad.login('consumer', 'token', 'secret')
self.assertEquals(len(caught), 1)
self.assertEquals(caught[0].category, DeprecationWarning)
def test_get_token_and_login_is_deprecated(self):
# get_token_and_login() works but triggers a deprecation warning.
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
NoNetworkLaunchpad.get_token_and_login('consumer')
self.assertEquals(len(caught), 1)
self.assertEquals(caught[0].category, DeprecationWarning)
class TestCredenitialSaveFailedCallback(unittest.TestCase):
# There is a callback which will be called if saving the credentials
# fails.
def setUp(self):
# launchpadlib.launchpad uses the socket module to look up the
# hostname, obviously that can vary so we replace the socket module
# with a fake that returns a fake hostname.
launchpadlib.launchpad.socket = FauxSocketModule()
self.temp_dir = tempfile.mkdtemp()
def tearDown(self):
launchpadlib.launchpad.socket = socket
shutil.rmtree(self.temp_dir)
def test_credentials_save_failed(self):
# If saving the credentials did not succeed and a callback was
# provided, it is called.
callback_called = []
def callback():
# Since we can't rebind "callback_called" here, we'll have to
# settle for mutating it to signal success.
callback_called.append(None)
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
service_root = "http://api.example.com/"
with fake_keyring(BadSaveKeyring()):
NoNetworkLaunchpad.login_with(
'not important', service_root=service_root,
launchpadlib_dir=launchpadlib_dir,
credential_save_failed=callback)
self.assertEquals(len(callback_called), 1)
def test_default_credentials_save_failed_is_to_raise_exception(self):
# If saving the credentials did not succeed and no callback was
# provided, the underlying exception is raised.
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
service_root = "http://api.example.com/"
with fake_keyring(BadSaveKeyring()):
self.assertRaises(
RuntimeError,
NoNetworkLaunchpad.login_with,
'not important', service_root=service_root,
launchpadlib_dir=launchpadlib_dir)
class TestMultipleSites(unittest.TestCase):
# If the same application name (consumer name) is used to access more than
# one site, the credentials need to be stored seperately. Therefore, the
# "username" passed ot the keyring includes the service root.
def setUp(self):
# launchpadlib.launchpad uses the socket module to look up the
# hostname, obviously that can vary so we replace the socket module
# with a fake that returns a fake hostname.
launchpadlib.launchpad.socket = FauxSocketModule()
self.temp_dir = tempfile.mkdtemp()
def tearDown(self):
launchpadlib.launchpad.socket = socket
shutil.rmtree(self.temp_dir)
def test_components_of_application_key(self):
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
keyring = InMemoryKeyring()
service_root = 'http://api.example.com/'
application_name = 'Super App 3000'
with fake_keyring(keyring):
launchpad = NoNetworkLaunchpad.login_with(
application_name, service_root=service_root,
launchpadlib_dir=launchpadlib_dir)
consumer_name = launchpad.credentials.consumer.key
application_key = keyring.data.keys()[0][1]
# Both the consumer name (normally the name of the application) and
# the service root (the URL of the service being accessed) are
# included in the key when storing credentials.
self.assert_(service_root in application_key)
self.assert_(consumer_name in application_key)
# The key used to store the credentials is of this structure (and
# shouldn't change between releases or stored credentials will be
# "forgotten").
self.assertEquals(application_key, consumer_name + '@' + service_root)
def test_same_app_different_servers(self):
launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
keyring = InMemoryKeyring()
# Be paranoid about the keyring starting out empty.
assert not keyring.data, 'oops, a fresh keyring has data in it'
with fake_keyring(keyring):
# Create stored credentials for the same application but against
# two different sites (service roots).
NoNetworkLaunchpad.login_with(
'application name', service_root='http://alpha.example.com/',
launchpadlib_dir=launchpadlib_dir)
NoNetworkLaunchpad.login_with(
'application name', service_root='http://beta.example.com/',
launchpadlib_dir=launchpadlib_dir)
# There should only be two sets of stored credentials (this assertion
# is of the test mechanism, not a test assertion).
assert len(keyring.data.keys()) == 2
application_key_1 = keyring.data.keys()[0][1]
application_key_2 = keyring.data.keys()[1][1]
self.assertNotEqual(application_key_1, application_key_2)
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)
launchpadlib-1.10.2/src/launchpadlib/README.txt 0000644 0000000 0000000 00000001350 11775055270 017642 0 ustar root root ..
This file is part of launchpadlib.
launchpadlib is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, version 3 of the License.
launchpadlib is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with launchpadlib. If not, see .
launchpadlib
************
See https://help.launchpad.net/API/launchpadlib .
launchpadlib-1.10.2/src/launchpadlib/docs/ 0000755 0000000 0000000 00000000000 11775571157 017104 5 ustar root root launchpadlib-1.10.2/src/launchpadlib/docs/toplevel.txt 0000644 0000000 0000000 00000006317 11775055270 021477 0 ustar root root The launchpad web service's top-level collections provide access to
Launchpad-wide objects like projects and people.
>>> import httplib2
>>> httplib2.debuglevel = 1
>>> from launchpadlib.testing.helpers import salgado_with_full_permissions
>>> launchpad = salgado_with_full_permissions.login()
connect: ...
...
It's possible to do key-based lookups on the top-level
collections. The bug collection does lookups by bug ID.
>>> bug = launchpad.bugs[1]
send: 'GET /.../bugs/1 ...'
...
To avoid triggering an HTTP request when simply looking up an object,
you can use a different syntax:
>>> bug = launchpad.bugs(1)
The HTTP request will happen when you need information that can only
be obtained from the web service.
>>> print bug.id
send: 'GET /.../bugs/1 ...'
...
1
Let's look at some more collections. The project collection does
lookups by project name.
>>> project = launchpad.projects('firefox')
>>> print project.name
send: 'GET /.../firefox ...'
...
firefox
The project group collection does lookups by project group name.
>>> group = launchpad.project_groups('gnome')
>>> print group.name
send: 'GET /.../gnome ...'
...
gnome
The distribution collection does lookups by distribution name.
>>> distribution = launchpad.distributions('ubuntu')
>>> print distribution.name
send: 'GET /.../ubuntu ...'
...
ubuntu
The person collection does lookups by a person's Launchpad
name.
>>> person = launchpad.people('salgado')
>>> print person.name
send: 'GET /.../~salgado ...'
...
salgado
>>> team = launchpad.people('rosetta-admins')
>>> print team.name
send: 'GET /1.0/~rosetta-admins ...'
...
rosetta-admins
How does launchpadlib know that 'salgado' is a person and
'rosetta-admins' is a team?
>>> print person.resource_type_link
http://.../1.0/#person
>>> 'default_membership_period' in person.lp_attributes
False
>>> print team.resource_type_link
http://.../1.0/#team
>>> 'default_membership_period' in team.lp_attributes
True
The truth is that it doesn't know, not before making that HTTP
request. Until an HTTP request is made, launchpadlib assumes
everything in launchpad.people[] is a team (since a team has strictly
more capabilities than a person).
>>> person2 = launchpad.people('salgado')
>>> 'default_membership_period' in person2.lp_attributes
True
But accessing any attribute of an object--even trying to see what kind
of object 'salgado' is--will trigger the HTTP request that will
determine that 'salgado' is actually a person.
>>> print person2.resource_type_link
send: 'GET /.../~salgado ...'
...
http://.../1.0/#person
>>> 'default_membership_period' in person2.lp_attributes
False
Accessing an attribute of an object that might be a team will trigger
the HTTP request, and then cause an error if the object turns out not
to be a team.
>>> person3 = launchpad.people('salgado')
>>> person3.default_membership_period
Traceback (most recent call last):
AttributeError: ...api.launchpad.../~salgado object has no attribute 'default_membership_period'
Cleanup.
>>> httplib2.debuglevel = None
launchpadlib-1.10.2/src/launchpadlib/docs/hosted-files.txt 0000644 0000000 0000000 00000005624 11775055270 022233 0 ustar root root ************
Hosted files
************
The Launchpad web service sets restrictions on what kinds of documents
can be written to a particular file. This test shows what happens when
you try to upload a non-image for a field that expects an image.
>>> from launchpadlib.testing.helpers import salgado_with_full_permissions
>>> launchpad = salgado_with_full_permissions.login()
>>> from launchpadlib.errors import HTTPError
>>> mugshot = launchpad.me.mugshot
>>> file_handle = mugshot.open("w", "image/png", "nonimage.txt")
>>> file_handle.content_type
'image/png'
>>> file_handle.filename
'nonimage.txt'
>>> file_handle.write("Not an image.")
>>> try:
... file_handle.close()
... except HTTPError, e:
... print e.content
The file uploaded was not recognized as an image; please
check it and retry.
Of course, uploading an image works fine.
>>> import os
>>> def load_image(filename):
... image_file = os.path.join(
... os.path.dirname(__file__), 'files', filename)
... return open(image_file).read()
>>> image = load_image("mugshot.png")
>>> len(image)
2260
>>> file_handle = mugshot.open("w", "image/png", "a-mugshot.png")
>>> file_handle.write(image)
>>> file_handle.close()
== Error handling ==
The server may set restrictions on what kinds of documents can be
written to a particular file.
>>> file_handle = mugshot.open("w", "image/png", "nonimage.txt")
>>> file_handle.content_type
'image/png'
>>> file_handle.filename
'nonimage.txt'
>>> file_handle.write("Not an image.")
>>> file_handle.close()
Traceback (most recent call last):
...
BadRequest: HTTP Error 400: Bad Request
...
== Caching ==
Hosted file resources implement the normal server-side caching
mechanism.
>>> file_handle = mugshot.open("w", "image/png", "image.png")
>>> file_handle.write(image)
>>> file_handle.close()
>>> import httplib2
>>> httplib2.debuglevel = 1
>>> launchpad = salgado_with_full_permissions.login()
connect: ...
>>> mugshot = launchpad.me.mugshot
send: ...
The first request for a file retrieves the file from the server.
>>> len(mugshot.open().read())
send: ...
reply: 'HTTP/1.1 303 See Other...
reply: 'HTTP/1.1 200 OK...
2260
The second request retrieves the file from the cache. After receiving
the 303 request with its Location header, no further HTTP requests are
issued because the Librarian's Cache-Control: headers tell us we
already have a fresh copy.
>>> len(mugshot.open().read())
send: ...
reply: 'HTTP/1.1 303 See Other...
header: Location: http://.../image.png
...
header: Content-Type: text/plain
2260
Finally, some cleanup code that deletes the mugshot.
>>> mugshot.delete()
send: 'DELETE...
reply: 'HTTP/1.1 200...
>>> httplib2.debuglevel = 0
launchpadlib-1.10.2/src/launchpadlib/docs/introduction.txt 0000644 0000000 0000000 00000031472 11775570504 022370 0 ustar root root ************
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.
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)
[...]
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)
>>> 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)
>>> stream.seek(0, 0)
>>> print ''.join(sorted(encrypted_file))
[1]
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):
...
Unauthorized: HTTP Error 401: Unauthorized
...
>>> salgado.display_name = "This won't work."
>>> salgado.lp_save()
Traceback (most recent call last):
...
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")
>>> sorted(launchpad.people)
[...]
>>> launchpad.me
Traceback (most recent call last):
...
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')
>>> authorization_url
'http://launchpad.dev: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.dev: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.
>>> sorted(dictionary.keys())
['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(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 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):
...
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):
...
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):
...
Unauthorized: HTTP Error 401: Unauthorized
...
Clean up
========
>>> os.remove(path)
launchpadlib-1.10.2/src/launchpadlib/docs/people.txt 0000644 0000000 0000000 00000014136 11775055270 021127 0 ustar root root = People and Teams =
The Launchpad web service, like Launchpad itself, exposes a unified
interface to people and teams. In other words, people and teams
occupy the same namespace. You treat people and teams as the same
type of object, and need to inspect the object to know whether you're
dealing with a person or a team.
== People ==
You can access Launchpad people through the web service interface.
The list of people is available from the service root.
>>> from launchpadlib.testing.helpers import salgado_with_full_permissions
>>> launchpad = salgado_with_full_permissions.login()
>>> people = launchpad.people
The list of people is not fetched until you actually use data.
>>> print people._wadl_resource.representation
None
>>> len(people)
4
>>> print people._wadl_resource.representation
{...}
The 'me' attribute is also available from the service root. It's a
quick way to get a reference to your own user account.
>>> me = launchpad.me
>>> me.name
u'salgado'
You can find a person by name.
>>> salgado = launchpad.people['salgado']
>>> salgado.name
u'salgado'
>>> salgado.display_name
u'Guilherme Salgado'
>>> salgado.is_team
False
But if no person by that name is registered, you get the expected KeyError.
>>> launchpad.people['not-a-registered-person']
Traceback (most recent call last):
...
KeyError: 'not-a-registered-person'
It's not possible to slice a single person from the top-level
collection of people. launchpadlib will try to use the value you pass
in as a person's name, which will almost always fail.
>>> launchpad.people[1]
Traceback (most recent call last):
...
KeyError: 1
You can find a person by email.
>>> email = salgado.preferred_email_address.email
>>> salgado = launchpad.people.getByEmail(email=email)
>>> salgado.name
u'salgado'
Besides a name and a display name, a person has many other attributes that you
can read.
XXX 05-Jun-2008 BarryWarsaw Some of these attributes are links to further
collections and are not yet tested. Tests will be added in future
branches.
>>> salgado.karma
0
>>> print salgado.homepage_content
None
>>> #salgado.mugshot
>>> #salgado.languages
>>> salgado.hide_email_addresses
False
>>> salgado.date_created
datetime.datetime(2005, 6, 6, 8, 59, 51, 596025, ...)
>>> print salgado.time_zone
None
>>> salgado.is_valid
True
>>> #salgado.wiki_names
>>> #salgado.irc_nicknames
>>> #salgado.jabber_ids
>>> #salgado.team_memberships
>>> #salgado.open_membership_invitations
>>> #salgado.teams_participated_in
>>> #salgado.teams_indirectly_participated_in
>>> #salgado.confirmed_email_addresses
>>> #salgado.preferred_email_address
>>> salgado.mailing_list_auto_subscribe_policy
u'Ask me when I join a team'
>>> salgado.visibility
u'Public'
== Teams ==
You also access teams using the same interface.
>>> team = launchpad.people['ubuntu-team']
>>> team.name
u'ubuntu-team'
>>> team.display_name
u'Ubuntu Team'
>>> team.is_team
True
Regular people have team attributes, but they're not used.
>>> print salgado.team_owner
None
You can find out how a person has membership in a team.
# XXX: salgado, 2008-08-01: Commented because method has been Unexported;
# it should be re-enabled after the operation is exported again.
# >>> path = salgado.findPathToTeam(
# ... team=launchpad.people['mailing-list-experts'])
# >>> [team.name for team in path]
# [u'admins', u'mailing-list-experts']
You can create a new team through the web interface. The simplest case of
this requires only the new team's name, owner and display name.
>>> launchpad.people['bassists']
Traceback (most recent call last):
...
KeyError: 'bassists'
>>> bassists = launchpad.people.newTeam(
... name='bassists', display_name='Awesome Rock Bass Players')
>>> bassists.name
u'bassists'
>>> bassists.display_name
u'Awesome Rock Bass Players'
>>> bassists.is_team
True
And of course, that team is now accessible directly.
>>> bassists = launchpad.people['bassists']
>>> bassists.name
u'bassists'
>>> bassists.display_name
u'Awesome Rock Bass Players'
You cannot create the same team twice.
>>> launchpad.people.newTeam(name='bassists', display_name='Bass Gods')
Traceback (most recent call last):
...
BadRequest: HTTP Error 400: Bad Request
...
Actually, the exception contains other useful information.
>>> from launchpadlib.errors import HTTPError
>>> try:
... launchpad.people.newTeam(
... name='bassists', display_name='Bass Gods')
... except HTTPError, error:
... pass
>>> error.response['status']
'400'
>>> error.content
'name: bassists is already in use by another person or team.'
Besides a name and a display name, a team has many other attributes that you
can read.
>>> bassists.karma
0
>>> print bassists.homepage_content
None
>>> bassists.hide_email_addresses
False
>>> bassists.date_created
datetime.datetime(...)
>>> print bassists.time_zone
None
>>> bassists.is_valid
True
>>> #bassists.team_memberships
>>> #bassists.open_membership_invitations
>>> #bassists.teams_participated_in
>>> #bassists.teams_indirectly_participated_in
>>> #bassists.confirmed_email_addresses
>>> #bassists.team_owner
>>> #bassists.preferred_email_address
>>> #bassists.members
>>> #bassists.admins
>>> #bassists.participants
>>> #bassists.deactivated_members
>>> #bassists.expired_members
>>> #bassists.invited_members
>>> #bassists.member_memberships
>>> #bassists.proposed_members
>>> bassists.visibility
u'Public'
>>> print bassists.team_description
None
>>> bassists.subscription_policy
u'Moderated Team'
>>> bassists.renewal_policy
u'invite them to apply for renewal'
>>> print bassists.default_membership_period
None
>>> print bassists.default_renewal_period
None
launchpadlib-1.10.2/src/launchpadlib/docs/operations.txt 0000644 0000000 0000000 00000001532 11775055270 022022 0 ustar root root ****************
Named operations
****************
launchpadlib can transparently determine the size of the list even
when the size is not directly provided, but is only available through
a link.
>>> from launchpadlib.testing.helpers import salgado_with_full_permissions
>>> launchpad = salgado_with_full_permissions.login(version="devel")
>>> results = launchpad.people.find(text='s')
>>> 'total_size' in results._wadl_resource.representation.keys()
False
>>> 'total_size_link' in results._wadl_resource.representation.keys()
True
>>> len(results) > 1
True
Of course, launchpadlib can also determine the size when the size _is_
directly provided.
>>> results = launchpad.people.find(text='salgado')
>>> 'total_size' in results._wadl_resource.representation.keys()
True
>>> len(results) == 1
True
launchpadlib-1.10.2/src/launchpadlib/docs/files/ 0000755 0000000 0000000 00000000000 11775571157 020206 5 ustar root root launchpadlib-1.10.2/src/launchpadlib/docs/files/mugshot.png 0000644 0000000 0000000 00000004324 11775055270 022376 0 ustar root root PNG
IHDR ݾP sRGB pHYs tIME ; tEXtComment Created with GIMPW AIDATx]HSm 3?pe[.`)")>aA7 9*SCHAHdD}@B7Q4uYJQjދe:*ڈ<ٲD"UC @@ @@ @@ @@ @@ @@ @@ K(RE`0($IEA@Xή.YEAEIJJJ^&
r,?R 4\9EQԹχdY$I$\9YG Hszԩes"j^GEQx<߹Zs_#kr-m (,XXd~Z_\IHZs
FbTsjOC$q+cZE\tYV-eXfggCZF>Y">_22 @@ @@ @@ @@ @@ @@ @@ XB(,AA<$I(:2hB!Am۶{Q'z@;;;dYVEQ%Ip4W`[ZZ};??GFFFFF#4==*IҟG^]]===1`srr9^o_Q/EQ?xbSS:ɛ(CCC===J^^RZZնQR^tv_-?>POOOVVVeJnnݻwW4www\z}0g@jC7oܾ}rSFFFcc{痵ۅjooiRU~47cZ?ܜC:!s-H_ R~ҥjݱcwoqFUUU8^mꢢk
ݻw~%l7ov:333Nszz:555'%% nOMMOp:>|۲5o߾2QgYvvυn7IZ[l_drrree3/g(O>~^[JJӧ=jGuY
8+jɓ'v[$..СCgﯨyo
=: uµhonllLKK[t_|i#dsssWqklÇʈyŋ@ ?666336l$?_@1y355'NŒ2RSSggg].WjjjAAAaa4v" @@ @@ @@ }ւ*L IENDB` launchpadlib-1.10.2/src/launchpadlib/docs/command-line.txt 0000644 0000000 0000000 00000002041 11775055270 022176 0 ustar root root ********************
Command-line scripts
********************
Launchpad includes one command-line script to make Launchpad
integration easier for third-party libraries that aren't written in
Python.
This file tests the workflow underlying the command-line script as
best it can.
RequestTokenApp
===============
This class is called by the command-line script
launchpad-request-token. It creates a request token on a given
Launchpad installation, and returns a JSON description of the request
token and the available access levels.
>>> import simplejson
>>> from launchpadlib.apps import RequestTokenApp
>>> web_root = "http://launchpad.dev:8085/"
>>> consumer_name = "consumer"
>>> token_app = RequestTokenApp(web_root, consumer_name, "context")
>>> json = simplejson.loads(token_app.run())
>>> sorted(json.keys())
['access_levels', 'lp.context', 'oauth_token',
'oauth_token_consumer', 'oauth_token_secret']
>>> print json['lp.context']
context
>>> print json['oauth_token_consumer']
consumer
launchpadlib-1.10.2/src/launchpadlib/launchpad.py 0000644 0000000 0000000 00000064662 11775055270 020474 0 ustar root root # Copyright 2008-2009 Canonical Ltd.
# This file is part of launchpadlib.
#
# launchpadlib is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with launchpadlib. If not, see .
"""Root Launchpad API class."""
__metaclass__ = type
__all__ = [
'Launchpad',
]
import os
import urlparse
import warnings
from lazr.restfulclient.resource import (
CollectionWithKeyBasedLookup,
HostedFile, # Re-import for client convenience
ScalarValue, # Re-import for client convenience
ServiceRoot,
)
from lazr.restfulclient.authorize.oauth import SystemWideConsumer
from lazr.restfulclient._browser import RestfulHttp
from launchpadlib.credentials import (
AccessToken,
AnonymousAccessToken,
AuthorizeRequestTokenWithBrowser,
Consumer,
Credentials,
KeyringCredentialStore,
UnencryptedFileCredentialStore,
)
from launchpadlib import uris
# Import old constants for backwards compatibility
from launchpadlib.uris import STAGING_SERVICE_ROOT, EDGE_SERVICE_ROOT
OAUTH_REALM = 'https://api.launchpad.net'
class PersonSet(CollectionWithKeyBasedLookup):
"""A custom subclass capable of person lookup by username."""
def _get_url_from_id(self, key):
"""Transform a username into the URL to a person resource."""
return str(self._root._root_uri.ensureSlash()) + '~' + str(key)
# The only way to determine whether a string corresponds to a
# person or a team object is to ask the server, so looking up an
# entry from the PersonSet always requires making an HTTP request.
collection_of = 'team'
class BugSet(CollectionWithKeyBasedLookup):
"""A custom subclass capable of bug lookup by bug ID."""
def _get_url_from_id(self, key):
"""Transform a bug ID into the URL to a bug resource."""
return str(self._root._root_uri.ensureSlash()) + 'bugs/' + str(key)
collection_of = 'bug'
class PillarSet(CollectionWithKeyBasedLookup):
"""A custom subclass capable of lookup by pillar name.
Projects, project groups, and distributions are all pillars.
"""
def _get_url_from_id(self, key):
"""Transform a project name into the URL to a project resource."""
return str(self._root._root_uri.ensureSlash()) + str(key)
# The subclasses for projects, project groups, and distributions
# all define this property differently.
collection_of = None
class ProjectSet(PillarSet):
"""A custom subclass for accessing the collection of projects."""
collection_of = 'project'
class ProjectGroupSet(PillarSet):
"""A custom subclass for accessing the collection of project groups."""
collection_of = 'project_group'
class DistributionSet(PillarSet):
"""A custom subclass for accessing the collection of project groups."""
collection_of = 'distribution'
class LaunchpadOAuthAwareHttp(RestfulHttp):
"""Detects expired/invalid OAuth tokens and tries to get a new token."""
def __init__(self, launchpad, authorization_engine, *args):
self.launchpad = launchpad
self.authorization_engine = authorization_engine
super(LaunchpadOAuthAwareHttp, self).__init__(*args)
def _bad_oauth_token(self, response, content):
"""Helper method to detect an error caused by a bad OAuth token."""
return (response.status == 401 and
(content.startswith("Expired token")
or content.startswith("Invalid token")
or content.startswith("Unknown access token")))
def _request(self, *args):
response, content = super(
LaunchpadOAuthAwareHttp, self)._request(*args)
return self.retry_on_bad_token(response, content, *args)
def retry_on_bad_token(self, response, content, *args):
"""If the response indicates a bad token, get a new token and retry.
Otherwise, just return the response.
"""
if (self._bad_oauth_token(response, content)
and self.authorization_engine is not None):
# This access token is bad. Scrap it and create a new one.
self.launchpad.credentials.access_token = None
self.authorization_engine(
self.launchpad.credentials, self.launchpad.credential_store)
# Retry the request with the new credentials.
return self._request(*args)
return response, content
class Launchpad(ServiceRoot):
"""Root Launchpad API class.
:ivar credentials: The credentials instance used to access Launchpad.
:type credentials: `Credentials`
"""
DEFAULT_VERSION = '1.0'
RESOURCE_TYPE_CLASSES = {
'bugs': BugSet,
'distributions': DistributionSet,
'people': PersonSet,
'project_groups': ProjectGroupSet,
'projects': ProjectSet,
}
RESOURCE_TYPE_CLASSES.update(ServiceRoot.RESOURCE_TYPE_CLASSES)
def __init__(self, credentials, authorization_engine,
credential_store, service_root=uris.STAGING_SERVICE_ROOT,
cache=None, timeout=None, proxy_info=None,
version=DEFAULT_VERSION):
"""Root access to the Launchpad API.
:param credentials: The credentials used to access Launchpad.
:type credentials: `Credentials`
:param authorization_engine: The object used to get end-user input
for authorizing OAuth request tokens. Used when an OAuth
access token expires or becomes invalid during a
session, or is discovered to be invalid once launchpadlib
starts up.
:type authorization_engine: `RequestTokenAuthorizationEngine`
:param service_root: The URL to the root of the web service.
:type service_root: string
"""
service_root = uris.lookup_service_root(service_root)
if (service_root.endswith(version)
or service_root.endswith(version + '/')):
error = ("It looks like you're using a service root that "
"incorporates the name of the web service version "
'("%s"). Please use one of the constants from '
"launchpadlib.uris instead, or at least remove "
"the version name from the root URI." % version)
raise ValueError(error)
self.credential_store = credential_store
# We already have an access token, but it might expire or
# become invalid during use. Store the authorization engine in
# case we need to authorize a new token during use.
self.authorization_engine = authorization_engine
super(Launchpad, self).__init__(
credentials, service_root, cache, timeout, proxy_info, version)
def httpFactory(self, credentials, cache, timeout, proxy_info):
return LaunchpadOAuthAwareHttp(
self, self.authorization_engine, credentials, cache, timeout,
proxy_info)
@classmethod
def authorization_engine_factory(cls, *args):
return AuthorizeRequestTokenWithBrowser(*args)
@classmethod
def credential_store_factory(cls, credential_save_failed):
return KeyringCredentialStore(credential_save_failed)
@classmethod
def login(cls, consumer_name, token_string, access_secret,
service_root=uris.STAGING_SERVICE_ROOT,
cache=None, timeout=None, proxy_info=None,
authorization_engine=None, allow_access_levels=None,
max_failed_attempts=None, credential_store=None,
credential_save_failed=None, version=DEFAULT_VERSION):
"""Convenience method for setting up access credentials.
When all three pieces of credential information (the consumer
name, the access token and the access secret) are available, this
method can be used to quickly log into the service root.
This method is deprecated as of launchpadlib version
1.9.0. You should use Launchpad.login_anonymously() for
anonymous access, and Launchpad.login_with() for all other
purposes.
:param consumer_name: the application name.
:type consumer_name: string
:param token_string: the access token, as appropriate for the
`AccessToken` constructor
:type token_string: string
:param access_secret: the access token's secret, as appropriate for
the `AccessToken` constructor
:type access_secret: string
:param service_root: The URL to the root of the web service.
:type service_root: string
:param authorization_engine: See `Launchpad.__init__`. If you don't
provide an authorization engine, a default engine will be
constructed using your values for `service_root` and
`credential_save_failed`.
:param allow_access_levels: This argument is ignored, and only
present to preserve backwards compatibility.
:param max_failed_attempts: This argument is ignored, and only
present to preserve backwards compatibility.
:return: The web service root
:rtype: `Launchpad`
"""
cls._warn_of_deprecated_login_method("login")
access_token = AccessToken(token_string, access_secret)
credentials = Credentials(
consumer_name=consumer_name, access_token=access_token)
if authorization_engine is None:
authorization_engine = cls.authorization_engine_factory(
service_root, consumer_name, allow_access_levels)
if credential_store is None:
credential_store = cls.credential_store_factory(
credential_save_failed)
return cls(credentials, authorization_engine, credential_store,
service_root, cache, timeout, proxy_info, version)
@classmethod
def get_token_and_login(cls, consumer_name,
service_root=uris.STAGING_SERVICE_ROOT,
cache=None, timeout=None, proxy_info=None,
authorization_engine=None, allow_access_levels=[],
max_failed_attempts=None, credential_store=None,
credential_save_failed=None,
version=DEFAULT_VERSION):
"""Get credentials from Launchpad and log into the service root.
This method is deprecated as of launchpadlib version
1.9.0. You should use Launchpad.login_anonymously() for
anonymous access and Launchpad.login_with() for all other
purposes.
:param consumer_name: Either a consumer name, as appropriate for
the `Consumer` constructor, or a premade Consumer object.
:type consumer_name: string
:param service_root: The URL to the root of the web service.
:type service_root: string
:param authorization_engine: See `Launchpad.__init__`. If you don't
provide an authorization engine, a default engine will be
constructed using your values for `service_root` and
`credential_save_failed`.
:param allow_access_levels: This argument is ignored, and only
present to preserve backwards compatibility.
:return: The web service root
:rtype: `Launchpad`
"""
cls._warn_of_deprecated_login_method("get_token_and_login")
return cls._authorize_token_and_login(
consumer_name, service_root, cache, timeout, proxy_info,
authorization_engine, allow_access_levels,
credential_store, credential_save_failed, version)
@classmethod
def _authorize_token_and_login(
cls, consumer_name, service_root, cache, timeout, proxy_info,
authorization_engine, allow_access_levels, credential_store,
credential_save_failed, version):
"""Authorize a request token. Log in with the resulting access token.
This is the private, non-deprecated implementation of the
deprecated method get_token_and_login(). Once
get_token_and_login() is removed, this code can be streamlined
and moved into its other call site, login_with().
"""
if isinstance(consumer_name, Consumer):
consumer = consumer_name
else:
# Create a system-wide consumer. lazr.restfulclient won't
# do this automatically, but launchpadlib's default is to
# do a desktop-wide integration.
consumer = SystemWideConsumer(consumer_name)
# Create the credentials with no Consumer, then set its .consumer
# property directly.
credentials = Credentials(None)
credentials.consumer = consumer
if authorization_engine is None:
authorization_engine = cls.authorization_engine_factory(
service_root, consumer_name, None, allow_access_levels)
if credential_store is None:
credential_store = cls.credential_store_factory(
credential_save_failed)
else:
# A credential store was passed in, so we won't be using
# any provided value for credential_save_failed. But at
# least make sure we weren't given a conflicting value,
# since that makes the calling code look confusing.
cls._assert_login_argument_consistency(
"credential_save_failed", credential_save_failed,
credential_store.credential_save_failed,
"credential_store")
# Try to get the credentials out of the credential store.
cached_credentials = credential_store.load(
authorization_engine.unique_consumer_id)
if cached_credentials is None:
# They're not there. Acquire new credentials using the
# authorization engine.
credentials = authorization_engine(credentials, credential_store)
else:
# We acquired credentials. But, the application name
# wasn't stored along with the credentials, because in a
# desktop integration scenario, a single set of
# credentials may be shared by many applications. We need
# to set the application name for this specific instance
# of the credentials.
credentials = cached_credentials
credentials.consumer.application_name = (
authorization_engine.application_name)
return cls(credentials, authorization_engine, credential_store,
service_root, cache, timeout, proxy_info, version)
@classmethod
def login_anonymously(
cls, consumer_name, service_root=uris.STAGING_SERVICE_ROOT,
launchpadlib_dir=None, timeout=None, proxy_info=None,
version=DEFAULT_VERSION):
"""Get access to Launchpad without providing any credentials."""
(service_root, launchpadlib_dir, cache_path,
service_root_dir) = cls._get_paths(service_root, launchpadlib_dir)
token = AnonymousAccessToken()
credentials = Credentials(consumer_name, access_token=token)
return cls(credentials, None, None, service_root=service_root,
cache=cache_path, timeout=timeout, proxy_info=proxy_info,
version=version)
@classmethod
def login_with(cls, application_name=None,
service_root=uris.STAGING_SERVICE_ROOT,
launchpadlib_dir=None, timeout=None, proxy_info=None,
authorization_engine=None, allow_access_levels=None,
max_failed_attempts=None, credentials_file=None,
version=DEFAULT_VERSION, consumer_name=None,
credential_save_failed=None, credential_store=None):
"""Log in to Launchpad, possibly acquiring and storing credentials.
Use this method to get a `Launchpad` object. If the end-user
has no cached Launchpad credential, their browser will open
and they'll be asked to log in and authorize a desktop
integration. The authorized Launchpad credential will be
stored securely: in the GNOME keyring, the KDE Wallet, or in
an encrypted file on disk.
The next time your program (or any other program run by that
user on the same computer) invokes this method, the end-user
will be prompted to unlock their keyring (or equivalent), and
the credential will be retrieved from local storage and
reused.
You can customize this behavior in three ways:
1. Pass in a filename to `credentials_file`. The end-user's
credential will be written to that file, and on subsequent
runs read from that file.
2. Subclass `CredentialStore` and pass in an instance of the
subclass as `credential_store`. This lets you change how
the end-user's credential is stored and retrieved locally.
3. Subclass `RequestTokenAuthorizationEngine` and pass in an
instance of the subclass as `authorization_engine`. This
lets you change change what happens when the end-user needs
to authorize the Launchpad credential.
:param application_name: The application name. This is *not*
the OAuth consumer name. Unless a consumer_name is also
provided, the OAuth consumer will be a system-wide
consumer representing the end-user's computer as a whole.
:type application_name: string
:param service_root: The URL to the root of the web service.
:type service_root: string. Can either be the full URL to a service
or one of the short service names.
:param launchpadlib_dir: The directory used to store cached
data obtained from Launchpad. The cache is shared by all
consumers, and each Launchpad service root has its own
cache.
:type launchpadlib_dir: string
:param authorization_engine: A strategy for getting the
end-user to authorize an OAuth request token, for
exchanging the request token for an access token, and for
storing the access token locally so that it can be
reused. By default, launchpadlib will open the end-user's
web browser to have them authorize the request token.
:type authorization_engine: `RequestTokenAuthorizationEngine`
:param allow_access_levels: The acceptable access levels for
this application.
This argument is used to construct the default
`authorization_engine`, so if you pass in your own
`authorization_engine` any value for this argument will be
ignored. This argument will also be ignored unless you
also specify `consumer_name`.
:type allow_access_levels: list of strings
:param max_failed_attempts: Ignored; only present for
backwards compatibility.
:param credentials_file: The path to a file in which to store
this user's OAuth access token.
:param version: The version of the Launchpad web service to use.
:param consumer_name: The consumer name, as appropriate for
the `Consumer` constructor. You probably don't want to
provide this, since providing it will prevent you from
taking advantage of desktop-wide integration.
:type consumer_name: string
:param credential_save_failed: a callback that is called upon
a failure to save the credentials locally. This argument is
used to construct the default `credential_store`, so if
you pass in your own `credential_store` any value for
this argument will be ignored.
:type credential_save_failed: A callable
:param credential_store: A strategy for storing an OAuth
access token locally. By default, tokens are stored in the
GNOME keyring (or equivalent). If `credentials_file` is
provided, then tokens are stored unencrypted in that file.
:type credential_store: `CredentialStore`
:return: A web service root authorized as the end-user.
:rtype: `Launchpad`
"""
(service_root, launchpadlib_dir, cache_path,
service_root_dir) = cls._get_paths(service_root, launchpadlib_dir)
if (application_name is None and consumer_name is None and
authorization_engine is None):
raise ValueError(
"At least one of application_name, consumer_name, or "
"authorization_engine must be provided.")
if credentials_file is not None and credential_store is not None:
raise ValueError(
"At most one of credentials_file and credential_store "
"must be provided.")
if credential_store is None:
if credentials_file is not None:
# The end-user wants credentials stored in an
# unencrypted file.
credential_store = UnencryptedFileCredentialStore(
credentials_file, credential_save_failed)
else:
credential_store = cls.credential_store_factory(
credential_save_failed)
else:
# A credential store was passed in, so we won't be using
# any provided value for credential_save_failed. But at
# least make sure we weren't given a conflicting value,
# since that makes the calling code look confusing.
cls._assert_login_argument_consistency(
'credential_save_failed', credential_save_failed,
credential_store.credential_save_failed,
"credential_store")
credential_store = credential_store
if authorization_engine is None:
authorization_engine = cls.authorization_engine_factory(
service_root, application_name, consumer_name,
allow_access_levels)
else:
# An authorization engine was passed in, so we won't be
# using any provided values for application_name,
# consumer_name, or allow_access_levels. But at least make
# sure we weren't given conflicting values, since that
# makes the calling code look confusing.
cls._assert_login_argument_consistency(
"application_name", application_name,
authorization_engine.application_name)
cls._assert_login_argument_consistency(
"consumer_name", consumer_name,
authorization_engine.consumer.key)
cls._assert_login_argument_consistency(
"allow_access_levels", allow_access_levels,
authorization_engine.allow_access_levels)
return cls._authorize_token_and_login(
authorization_engine.consumer, service_root,
cache_path, timeout, proxy_info, authorization_engine,
allow_access_levels, credential_store,
credential_save_failed, version)
@classmethod
def _warn_of_deprecated_login_method(cls, name):
warnings.warn(
("The Launchpad.%s() method is deprecated. You should use "
"Launchpad.login_anonymous() for anonymous access and "
"Launchpad.login_with() for all other purposes.") % name,
DeprecationWarning)
@classmethod
def _assert_login_argument_consistency(
cls, argument_name, argument_value, object_value,
object_name="authorization engine"):
"""Helper to find conflicting values passed into the login methods.
Many of the arguments to login_with are used to build other
objects--the authorization engine or the credential store. If
these objects are provided directly, many of the arguments
become redundant. We'll allow redundant arguments through, but
if a argument *conflicts* with the corresponding value in the
provided object, we raise an error.
"""
inconsistent_value_message = (
"Inconsistent values given for %s: "
"(%r passed in, versus %r in %s). "
"You don't need to pass in %s if you pass in %s, "
"so just omit that argument.")
if (argument_value is not None and argument_value != object_value):
raise ValueError(inconsistent_value_message % (
argument_name, argument_value, object_value,
object_name, argument_name, object_name))
@classmethod
def _get_paths(cls, service_root, launchpadlib_dir=None):
"""Locate launchpadlib-related user paths and ensure they exist.
This is a helper function used by login_with() and
login_anonymously().
:param service_root: The service root the user wants to
connect to. This may be an alias (which will be
dereferenced to a URL and returned) or a URL (which will
be returned as is).
:param launchpadlib_dir: The user's base launchpadlib
directory, if known. This may be modified, expanded, or
determined from the environment if missing. A definitive
value will be returned.
:return: A 4-tuple:
(service_root_uri, launchpadlib_dir, cache_dir, service_root_dir)
"""
if launchpadlib_dir is None:
launchpadlib_dir = os.path.join('~', '.launchpadlib')
launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
if launchpadlib_dir[:1] == '~':
raise ValueError("Must set $HOME or pass 'launchpadlib_dir' to "
"indicate location to store cached data")
if not os.path.exists(launchpadlib_dir):
os.makedirs(launchpadlib_dir, 0700)
os.chmod(launchpadlib_dir, 0700)
# Determine the real service root.
service_root = uris.lookup_service_root(service_root)
# Each service root has its own cache and credential dirs.
scheme, host_name, path, query, fragment = urlparse.urlsplit(
service_root)
service_root_dir = os.path.join(launchpadlib_dir, host_name)
cache_path = os.path.join(service_root_dir, 'cache')
if not os.path.exists(cache_path):
os.makedirs(cache_path, 0700)
return (service_root, launchpadlib_dir, cache_path, service_root_dir)
launchpadlib-1.10.2/setup.cfg 0000644 0000000 0000000 00000000073 11775571157 014560 0 ustar root root [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0