summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSVN-Git Migration <python-modules-team@lists.alioth.debian.org>2015-10-08 12:21:53 -0700
committerSVN-Git Migration <python-modules-team@lists.alioth.debian.org>2015-10-08 12:21:53 -0700
commit13a200cb0f1837e3410279328d42bea8bdd54be2 (patch)
treead6891ee3560f6595f808c628c73e836e57c49bc /src
Imported Upstream version 1.5.2
Diffstat (limited to 'src')
-rw-r--r--src/launchpadlib.egg-info/PKG-INFO81
-rw-r--r--src/launchpadlib.egg-info/SOURCES.txt28
-rw-r--r--src/launchpadlib.egg-info/dependency_links.txt1
-rw-r--r--src/launchpadlib.egg-info/not-zip-safe1
-rw-r--r--src/launchpadlib.egg-info/requires.txt7
-rw-r--r--src/launchpadlib.egg-info/top_level.txt1
-rw-r--r--src/launchpadlib/NEWS.txt45
-rw-r--r--src/launchpadlib/README.txt19
-rw-r--r--src/launchpadlib/__init__.py17
-rw-r--r--src/launchpadlib/credentials.py246
-rw-r--r--src/launchpadlib/docs/files/mugshot.pngbin0 -> 2260 bytes
-rw-r--r--src/launchpadlib/docs/hosted-files.txt104
-rw-r--r--src/launchpadlib/docs/introduction.txt335
-rw-r--r--src/launchpadlib/docs/people.txt218
-rw-r--r--src/launchpadlib/docs/toplevel.txt31
-rw-r--r--src/launchpadlib/errors.py20
-rw-r--r--src/launchpadlib/launchpad.py279
-rw-r--r--src/launchpadlib/testing/__init__.py0
-rw-r--r--src/launchpadlib/testing/helpers.py63
-rw-r--r--src/launchpadlib/tests/__init__.py16
-rw-r--r--src/launchpadlib/tests/test_credentials.py60
-rw-r--r--src/launchpadlib/tests/test_launchpad.py230
-rw-r--r--src/launchpadlib/wadl-to-refhtml.xsl1045
23 files changed, 2847 insertions, 0 deletions
diff --git a/src/launchpadlib.egg-info/PKG-INFO b/src/launchpadlib.egg-info/PKG-INFO
new file mode 100644
index 0000000..9d1c608
--- /dev/null
+++ b/src/launchpadlib.egg-info/PKG-INFO
@@ -0,0 +1,81 @@
+Metadata-Version: 1.0
+Name: launchpadlib
+Version: 1.5.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 <http://www.gnu.org/licenses/>.
+
+ launchpadlib
+ ************
+
+ See https://help.launchpad.net/API/launchpadlib .
+
+ =====================
+ NEWS for launchpadlib
+ =====================
+
+ 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
diff --git a/src/launchpadlib.egg-info/SOURCES.txt b/src/launchpadlib.egg-info/SOURCES.txt
new file mode 100644
index 0000000..606b40f
--- /dev/null
+++ b/src/launchpadlib.egg-info/SOURCES.txt
@@ -0,0 +1,28 @@
+COPYING.txt
+HACKING.txt
+README.txt
+ez_setup.py
+setup.py
+src/launchpadlib/NEWS.txt
+src/launchpadlib/README.txt
+src/launchpadlib/__init__.py
+src/launchpadlib/credentials.py
+src/launchpadlib/errors.py
+src/launchpadlib/launchpad.py
+src/launchpadlib/wadl-to-refhtml.xsl
+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/hosted-files.txt
+src/launchpadlib/docs/introduction.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/tests/__init__.py
+src/launchpadlib/tests/test_credentials.py
+src/launchpadlib/tests/test_launchpad.py \ No newline at end of file
diff --git a/src/launchpadlib.egg-info/dependency_links.txt b/src/launchpadlib.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/launchpadlib.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/src/launchpadlib.egg-info/not-zip-safe b/src/launchpadlib.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/launchpadlib.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/src/launchpadlib.egg-info/requires.txt b/src/launchpadlib.egg-info/requires.txt
new file mode 100644
index 0000000..d1cc813
--- /dev/null
+++ b/src/launchpadlib.egg-info/requires.txt
@@ -0,0 +1,7 @@
+httplib2
+lazr.restfulclient
+lazr.uri
+oauth
+setuptools
+simplejson
+wadllib \ No newline at end of file
diff --git a/src/launchpadlib.egg-info/top_level.txt b/src/launchpadlib.egg-info/top_level.txt
new file mode 100644
index 0000000..9dc228f
--- /dev/null
+++ b/src/launchpadlib.egg-info/top_level.txt
@@ -0,0 +1 @@
+launchpadlib
diff --git a/src/launchpadlib/NEWS.txt b/src/launchpadlib/NEWS.txt
new file mode 100644
index 0000000..06fb009
--- /dev/null
+++ b/src/launchpadlib/NEWS.txt
@@ -0,0 +1,45 @@
+=====================
+NEWS for launchpadlib
+=====================
+
+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
diff --git a/src/launchpadlib/README.txt b/src/launchpadlib/README.txt
new file mode 100644
index 0000000..fd68370
--- /dev/null
+++ b/src/launchpadlib/README.txt
@@ -0,0 +1,19 @@
+..
+ 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 <http://www.gnu.org/licenses/>.
+
+launchpadlib
+************
+
+See https://help.launchpad.net/API/launchpadlib .
diff --git a/src/launchpadlib/__init__.py b/src/launchpadlib/__init__.py
new file mode 100644
index 0000000..dae4935
--- /dev/null
+++ b/src/launchpadlib/__init__.py
@@ -0,0 +1,17 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+__version__ = '1.5.2'
diff --git a/src/launchpadlib/credentials.py b/src/launchpadlib/credentials.py
new file mode 100644
index 0000000..013c825
--- /dev/null
+++ b/src/launchpadlib/credentials.py
@@ -0,0 +1,246 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""launchpadlib credentials and authentication support."""
+
+__metaclass__ = type
+__all__ = [
+ 'AccessToken',
+ 'Consumer',
+ 'Credentials',
+ ]
+
+from ConfigParser import SafeConfigParser
+import cgi
+import httplib2
+from oauth.oauth import OAuthConsumer, OAuthToken
+from urllib import urlencode
+
+from lazr.restfulclient.errors import CredentialsFileError, HTTPError
+
+
+CREDENTIALS_FILE_VERSION = '1'
+STAGING_WEB_ROOT = 'https://staging.launchpad.net/'
+request_token_page = '+request-token'
+access_token_page = '+access-token'
+authorize_token_page = '+authorize-token'
+
+
+class Credentials:
+ """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
+
+ def __init__(self, consumer_name=None, consumer_secret='',
+ access_token=None):
+ """The user's Launchpad API credentials.
+
+ :param consumer_name: The name of the consumer (application)
+ :param consumer_secret: The secret of the consumer
+ :param access_token: The authenticated user access token
+ :type access_token: `AccessToken`
+ """
+ self.consumer = None
+ if consumer_name is not None:
+ self.consumer = Consumer(consumer_name, consumer_secret)
+ self.access_token = access_token
+
+ def load(self, readable_file):
+ """Load credentials from a file-like object.
+
+ This overrides the consumer and access token given in the constructor
+ and replaces them with the values read from the file.
+
+ :param readable_file: A file-like object to read the credentials from
+ :type readable_file: Any object supporting the file-like `read()`
+ method
+ """
+ # Attempt to load the access token from the file.
+ parser = SafeConfigParser()
+ parser.readfp(readable_file)
+ # Check the version number and extract the access token and
+ # secret. Then convert these to the appropriate instances.
+ if not parser.has_section(CREDENTIALS_FILE_VERSION):
+ raise CredentialsFileError('No configuration for version %s' %
+ CREDENTIALS_FILE_VERSION)
+ consumer_key = parser.get(
+ CREDENTIALS_FILE_VERSION, 'consumer_key')
+ consumer_secret = parser.get(
+ CREDENTIALS_FILE_VERSION, 'consumer_secret')
+ self.consumer = Consumer(consumer_key, consumer_secret)
+ access_token = parser.get(
+ CREDENTIALS_FILE_VERSION, 'access_token')
+ access_secret = parser.get(
+ CREDENTIALS_FILE_VERSION, 'access_secret')
+ self.access_token = AccessToken(access_token, access_secret)
+
+ @classmethod
+ def load_from_path(cls, path):
+ """Convenience method for loading credentials from a file.
+
+ Open the file, create the Credentials and load from the file,
+ and finally close the file and return the newly created
+ Credentials instance.
+
+ :param path: In which file the credential file should be saved.
+ :type path: string
+ :return: The loaded Credentials instance.
+ :rtype: `Credentials`
+ """
+ credentials = cls()
+ credentials_file = open(path, 'r')
+ credentials.load(credentials_file)
+ credentials_file.close()
+ return credentials
+
+ def save(self, writable_file):
+ """Write the credentials to the file-like object.
+
+ :param writable_file: A file-like object to write the credentials to
+ :type writable_file: Any object supporting the file-like `write()`
+ method
+ :raise CredentialsFileError: when there is either no consumer or no
+ access token
+ """
+ if self.consumer is None:
+ raise CredentialsFileError('No consumer')
+ if self.access_token is None:
+ raise CredentialsFileError('No access token')
+
+ parser = SafeConfigParser()
+ parser.add_section(CREDENTIALS_FILE_VERSION)
+ parser.set(CREDENTIALS_FILE_VERSION,
+ 'consumer_key', self.consumer.key)
+ parser.set(CREDENTIALS_FILE_VERSION,
+ 'consumer_secret', self.consumer.secret)
+ parser.set(CREDENTIALS_FILE_VERSION,
+ 'access_token', self.access_token.key)
+ parser.set(CREDENTIALS_FILE_VERSION,
+ 'access_secret', self.access_token.secret)
+ parser.write(writable_file)
+
+ def save_to_path(self, path):
+ """Convenience method for saving credentials to a file.
+
+ Create the file, call self.save(), and close the file. Existing
+ files are overwritten.
+
+ :param path: In which file the credential file should be saved.
+ :type path: string
+ """
+ credentials_file = open(path, 'w')
+ self.save(credentials_file)
+ credentials_file.close()
+
+ def get_request_token(self, context=None, web_root=STAGING_WEB_ROOT):
+ """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.
+ :return: The URL for the user to authorize the `OAuthToken` provided
+ by Launchpad.
+ """
+ assert self.consumer is not None, "Consumer not specified."
+ assert self.access_token is None, "Access token already obtained."
+ params = dict(
+ oauth_consumer_key=self.consumer.key,
+ oauth_signature_method='PLAINTEXT',
+ oauth_signature='&')
+ url = web_root + request_token_page
+ response, content = httplib2.Http().request(
+ url, method='POST', body=urlencode(params))
+ if response.status != 200:
+ raise HTTPError(response, content)
+ self._request_token = OAuthToken.from_string(content)
+ url = '%s%s?oauth_token=%s' % (web_root, authorize_token_page,
+ self._request_token.key)
+ if context is not None:
+ url += "&lp.context=%s" % context
+ return url
+
+ def exchange_request_token_for_access_token(
+ self, web_root=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.")
+ 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
+ response, content = httplib2.Http().request(
+ url, method='POST', body=urlencode(params))
+ if response.status != 200:
+ raise HTTPError(response, content)
+ self.access_token = AccessToken.from_string(content)
+
+
+# These two classes are provided for convenience (so applications don't need
+# to import from launchpadlib._oauth.oauth), and to provide a default argument
+# for secret.
+
+class Consumer(OAuthConsumer):
+ """An OAuth consumer (application)."""
+
+ def __init__(self, key, secret=''):
+ super(Consumer, self).__init__(key, secret)
+
+
+class AccessToken(OAuthToken):
+ """An OAuth access token."""
+
+ def __init__(self, key, secret='', context=None):
+ super(AccessToken, self).__init__(key, secret)
+ self.context = 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 key."
+ 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)
diff --git a/src/launchpadlib/docs/files/mugshot.png b/src/launchpadlib/docs/files/mugshot.png
new file mode 100644
index 0000000..94a7e76
--- /dev/null
+++ b/src/launchpadlib/docs/files/mugshot.png
Binary files differ
diff --git a/src/launchpadlib/docs/hosted-files.txt b/src/launchpadlib/docs/hosted-files.txt
new file mode 100644
index 0000000..40c2e52
--- /dev/null
+++ b/src/launchpadlib/docs/hosted-files.txt
@@ -0,0 +1,104 @@
+************
+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
+ <BLANKLINE>
+ 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):
+ ...
+ HTTPError: 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://localhost:58000/.../image.png
+ header: Vary: Cookie, Authorization, Accept
+ 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
diff --git a/src/launchpadlib/docs/introduction.txt b/src/launchpadlib/docs/introduction.txt
new file mode 100644
index 0000000..c06fefb
--- /dev/null
+++ b/src/launchpadlib/docs/introduction.txt
@@ -0,0 +1,335 @@
+= 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)
+ [...]
+
+For convenience, the application may store the credentials on the file system,
+so that the next time Salgado interacts with the application, he won't have
+to go through the whole OAuth request dance.
+
+ >>> import os
+ >>> import tempfile
+ >>> fd, path = tempfile.mkstemp('.credentials')
+ >>> os.close(fd)
+
+Once Salgado's credentials are obtained for the first time, just set the
+appropriate instance variables and use the save() method.
+
+ >>> credentials.consumer = consumer
+ >>> credentials.access_token = access_token
+ >>> credentials_file = open(path, 'w')
+ >>> credentials.save(credentials_file)
+ >>> credentials_file.close()
+
+And the credentials are perfectly valid for accessing Launchpad.
+
+ >>> launchpad = Launchpad(credentials=credentials)
+ >>> sorted(launchpad.people)
+ [...]
+ >>> sorted(launchpad.bugs)
+ [...]
+
+The credentials can also be retrieved from the file, so that the OAuth request
+dance can be avoided.
+
+ >>> credentials = Credentials()
+ >>> credentials_file = open(path)
+ >>> credentials.load(credentials_file)
+ >>> credentials_file.close()
+ >>> credentials.consumer.key
+ 'launchpad-library'
+ >>> credentials.consumer.secret
+ ''
+ >>> credentials.access_token.key
+ 'salgado-change-anything'
+ >>> credentials.access_token.secret
+ 'test'
+
+These credentials too, are perfectly usable to access Launchpad.
+
+ >>> launchpad = Launchpad(credentials=credentials)
+ >>> sorted(launchpad.people)
+ [...]
+ >>> sorted(launchpad.bugs)
+ [...]
+
+The security of the stored credentials is left up to the file-like object.
+Here, the application decides to use a dubious encryption algorithm to hide
+Salgado's credentials.
+
+ >>> from StringIO import StringIO
+ >>> from codecs import EncodedFile
+ >>> encrypted_file = StringIO()
+ >>> stream = EncodedFile(encrypted_file, 'rot_13', 'ascii')
+ >>> credentials.save(stream)
+ >>> print encrypted_file.getvalue()
+ [1]
+ pbafhzre_frperg =
+ npprff_gbxra = fnytnqb-punatr-nalguvat
+ pbafhzre_xrl = ynhapucnq-yvoenel
+ npprff_frperg = grfg
+ <BLANKLINE>
+ <BLANKLINE>
+
+ >>> stream.seek(0)
+ >>> credentials = Credentials()
+ >>> credentials.load(stream)
+ >>> credentials.consumer.key
+ 'launchpad-library'
+ >>> credentials.consumer.secret
+ ''
+ >>> credentials.access_token.key
+ 'salgado-change-anything'
+ >>> credentials.access_token.secret
+ 'test'
+
+
+== Convenience ==
+
+When the consumer name, access token and access secret are all known up-front,
+a convenience method is available for logging into the web service in one
+function call.
+
+ >>> launchpad = Launchpad.login(
+ ... 'launchpad-library', 'salgado-change-anything', 'test')
+ >>> sorted(launchpad.people)
+ [...]
+
+If that is not the case the application should obtain authorization from
+the user and get the credentials directly from Launchpad.
+
+First we must get a request token.
+
+ >>> import launchpadlib.credentials
+ >>> credentials = Credentials('consumer')
+ >>> authorization_url = credentials.get_request_token(
+ ... context='firefox', web_root='http://launchpad.dev:8085/')
+ >>> authorization_url
+ 'http://launchpad.dev:8085/+authorize-token?oauth_token=...&lp.context=firefox'
+
+Now the user must authorize that token, so we'll hand-craft a request
+to pretend the user is authorizing it.
+
+ >>> import httplib2
+ >>> from urllib import urlencode
+ >>> params = {'field.actions.WRITE_PRIVATE': 1,
+ ... 'oauth_token': credentials._request_token.key,
+ ... 'lp.context': 'firefox'}
+ >>> foo_bar_auth = 'Basic %s' % (
+ ... 'foo.bar@canonical.com:test'.encode('base64'))
+ >>> headers = {'Authorization': foo_bar_auth}
+ >>> response, content = httplib2.Http().request(
+ ... authorization_url, method='POST', body=urlencode(params),
+ ... headers=headers)
+ >>> response['status']
+ '200'
+
+After that we can exchange that request token for an access token.
+
+ >>> credentials.exchange_request_token_for_access_token(
+ ... web_root='http://launchpad.dev:8085/')
+
+Once that's done, our credentials will be complete and ready to use.
+
+ >>> credentials.consumer.key
+ 'consumer'
+ >>> credentials.access_token
+ <launchpadlib.credentials.AccessToken...
+ >>> credentials.access_token.key is not None
+ True
+ >>> credentials.access_token.secret is not None
+ True
+ >>> credentials.access_token.context
+ 'firefox'
+
+There's also a convenience method which does the access token
+negotiation and logs into the web service. It uses the methods
+documented above and once it has the request token's authorization URL
+it opens up a web browser for the user to authoriza it and asks him to
+come back and press <Enter> once that's done. When he does it, the
+request token is exchanged for an access token and the authentication is
+complete.
+
+ # Since this will open up a web browser we're not going to actually run it
+ # here.
+ >>> # consumer_name = 'launchpadlib'
+ >>> # launchpad = Launchpad.get_token_and_login(consumer_name)
+
+
+== Credentials file errors ==
+
+If the credentials file is empty, loading it raises an exception.
+
+ >>> credentials = Credentials()
+ >>> credentials.load(StringIO())
+ Traceback (most recent call last):
+ ...
+ CredentialsFileError: No configuration for version 1
+
+It is an error to save a credentials file when no consumer or access token is
+available.
+
+ >>> credentials.consumer = None
+ >>> credentials.save(StringIO())
+ Traceback (most recent call last):
+ ...
+ CredentialsFileError: No consumer
+
+ >>> credentials.consumer = consumer
+ >>> credentials.access_token = None
+ >>> credentials.save(StringIO())
+ Traceback (most recent call last):
+ ...
+ CredentialsFileError: No access token
+
+The credentials file is not intended to be edited, but because it's human
+readable, that's of course possible. If the credentials file gets corrupted,
+an error is raised.
+
+ >>> credentials_file = StringIO("""\
+ ... [1]
+ ... #consumer_key: aardvark
+ ... consumer_secret: badger
+ ... access_token: caribou
+ ... access_secret: dingo
+ ... """)
+ >>> credentials.load(credentials_file)
+ Traceback (most recent call last):
+ ...
+ NoOptionError: No option 'consumer_key' in section: '1'
+
+ >>> credentials_file = StringIO("""\
+ ... [1]
+ ... consumer_key: aardvark
+ ... #consumer_secret: badger
+ ... access_token: caribou
+ ... access_secret: dingo
+ ... """)
+ >>> credentials.load(credentials_file)
+ Traceback (most recent call last):
+ ...
+ NoOptionError: No option 'consumer_secret' in section: '1'
+
+ >>> credentials_file = StringIO("""\
+ ... [1]
+ ... consumer_key: aardvark
+ ... consumer_secret: badger
+ ... #access_token: caribou
+ ... access_secret: dingo
+ ... """)
+ >>> credentials.load(credentials_file)
+ Traceback (most recent call last):
+ ...
+ NoOptionError: No option 'access_token' in section: '1'
+
+ >>> credentials_file = StringIO("""\
+ ... [1]
+ ... consumer_key: aardvark
+ ... consumer_secret: badger
+ ... access_token: caribou
+ ... #access_secret: dingo
+ ... """)
+ >>> credentials.load(credentials_file)
+ Traceback (most recent call last):
+ ...
+ NoOptionError: No option 'access_secret' in section: '1'
+
+
+== Bad credentials ==
+
+The application is not allowed to access Launchpad if there are no
+credentials.
+
+ >>> credentials = Credentials(consumer)
+ >>> launchpad = Launchpad(credentials=credentials)
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 401: Unauthorized
+
+The application is not allowed to access Launchpad with a bad access token.
+
+ >>> access_token = AccessToken('bad', 'no-secret')
+ >>> credentials = Credentials(
+ ... consumer_name=consumer.key, consumer_secret=consumer.secret,
+ ... access_token=access_token)
+ >>> launchpad = Launchpad(credentials=credentials)
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 401: Unauthorized
+
+The application is not allowed to access Launchpad with a bad access secret.
+
+ >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret')
+ >>> credentials = Credentials(
+ ... consumer_name=consumer.key, consumer_secret=consumer.secret,
+ ... access_token=access_token)
+ >>> launchpad = Launchpad(credentials=credentials)
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 401: Unauthorized
+
+
+== Clean up ==
+
+ >>> os.remove(path)
diff --git a/src/launchpadlib/docs/people.txt b/src/launchpadlib/docs/people.txt
new file mode 100644
index 0000000..defc54d
--- /dev/null
+++ b/src/launchpadlib/docs/people.txt
@@ -0,0 +1,218 @@
+= 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):
+ ...
+ HTTPError: 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
diff --git a/src/launchpadlib/docs/toplevel.txt b/src/launchpadlib/docs/toplevel.txt
new file mode 100644
index 0000000..580e6ff
--- /dev/null
+++ b/src/launchpadlib/docs/toplevel.txt
@@ -0,0 +1,31 @@
+The launchpad web service's top-level collections provide access to
+Launchpad-wide objects like projects and people.
+
+ >>> from launchpadlib.testing.helpers import salgado_with_full_permissions
+ >>> launchpad = salgado_with_full_permissions.login()
+
+It's possible to do key-based lookups on the top-level
+collections. The bug collection does lookups by bug ID.
+
+ >>> launchpad.bugs[1].id
+ 1
+
+The person collection does lookups by a person's Launchpad name.
+
+ >>> launchpad.people['salgado'].name
+ u'salgado'
+
+The project collection does lookups by project name.
+
+ >>> launchpad.projects['firefox'].name
+ u'firefox'
+
+The project group collection does lookups by project group name.
+
+ >>> launchpad.project_groups['firefox'].name
+ u'firefox'
+
+The distribution collection does lookups by distribution name.
+
+ >>> launchpad.distributions['ubuntu'].name
+ u'ubuntu'
diff --git a/src/launchpadlib/errors.py b/src/launchpadlib/errors.py
new file mode 100644
index 0000000..b9e59a4
--- /dev/null
+++ b/src/launchpadlib/errors.py
@@ -0,0 +1,20 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+
+"""Reimport errors from restfulclient for convenience's sake."""
+
+from lazr.restfulclient.errors import *
diff --git a/src/launchpadlib/launchpad.py b/src/launchpadlib/launchpad.py
new file mode 100644
index 0000000..dbba313
--- /dev/null
+++ b/src/launchpadlib/launchpad.py
@@ -0,0 +1,279 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Root Launchpad API class."""
+
+__metaclass__ = type
+__all__ = [
+ 'Launchpad',
+ ]
+
+import os
+import stat
+import sys
+import urlparse
+import webbrowser
+
+from lazr.uri import URI
+from lazr.restfulclient._browser import RestfulHttp
+from lazr.restfulclient.resource import (
+ CollectionWithKeyBasedLookup, HostedFile, ServiceRoot)
+from launchpadlib.credentials import AccessToken, Credentials
+from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
+
+
+OAUTH_REALM = 'https://api.launchpad.net'
+LPNET_SERVICE_ROOT = 'https://api.launchpad.net/beta/'
+EDGE_SERVICE_ROOT = 'https://api.edge.launchpad.net/beta/'
+STAGING_SERVICE_ROOT = 'https://api.staging.launchpad.net/beta/'
+DEV_SERVICE_ROOT = 'https://api.launchpad.dev/beta/'
+DOGFOOD_SERVICE_ROOT = 'https://api.dogfood.launchpad.net/beta/'
+
+
+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)
+
+
+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)
+
+
+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)
+
+
+class Launchpad(ServiceRoot):
+ """Root Launchpad API class.
+
+ :ivar credentials: The credentials instance used to access Launchpad.
+ :type credentials: `Credentials`
+ """
+
+ RESOURCE_TYPE_CLASSES = {
+ 'bugs': BugSet,
+ 'distributions': PillarSet,
+ 'HostedFile': HostedFile,
+ 'people': PersonSet,
+ 'project_groups': PillarSet,
+ 'projects': PillarSet,
+ }
+
+ service_roots = dict(
+ production=LPNET_SERVICE_ROOT,
+ edge=EDGE_SERVICE_ROOT,
+ staging=STAGING_SERVICE_ROOT,
+ dogfood=DOGFOOD_SERVICE_ROOT,
+ dev=DEV_SERVICE_ROOT,
+ )
+
+
+ def __init__(self, credentials, service_root=STAGING_SERVICE_ROOT,
+ cache=None, timeout=None, proxy_info=None):
+ """Root access to the Launchpad API.
+
+ :param credentials: The credentials used to access Launchpad.
+ :type credentials: `Credentials`
+ :param service_root: The URL to the root of the web service.
+ :type service_root: string
+ """
+ super(Launchpad, self).__init__(
+ credentials, service_root, cache, timeout, proxy_info)
+
+ def httpFactory(self, credentials, cache, timeout, proxy_info):
+ return OAuthSigningHttp(credentials, cache, timeout, proxy_info)
+
+ @classmethod
+ def login(cls, consumer_name, token_string, access_secret,
+ service_root=STAGING_SERVICE_ROOT,
+ cache=None, timeout=None, proxy_info=None):
+ """Convenience 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.
+
+ :param consumer_name: the consumer name, as appropriate for the
+ `Consumer` constructor
+ :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
+ :return: The web service root
+ :rtype: `Launchpad`
+ """
+ access_token = AccessToken(token_string, access_secret)
+ credentials = Credentials(
+ consumer_name=consumer_name, access_token=access_token)
+ return cls(credentials, service_root, cache, timeout, proxy_info)
+
+ @classmethod
+ def get_token_and_login(cls, consumer_name,
+ service_root=STAGING_SERVICE_ROOT,
+ cache=None, timeout=None, proxy_info=None):
+ """Get credentials from Launchpad and log into the service root.
+
+ This is a convenience method which will open up the user's preferred
+ web browser and thus should not be used by most applications.
+ Applications should, instead, use Credentials.get_request_token() to
+ obtain the authorization URL and
+ Credentials.exchange_request_token_for_access_token() to obtain the
+ actual OAuth access token.
+
+ This method will negotiate an OAuth access token with the service
+ provider, but to complete it we will need the user to log into
+ Launchpad and authorize us, so we'll open the authorization page in
+ a web browser and ask the user to come back here and tell us when they
+ finished the authorization process.
+
+ :param consumer_name: The consumer name, as appropriate for the
+ `Consumer` constructor
+ :type consumer_name: string
+ :param service_root: The URL to the root of the web service.
+ :type service_root: string
+ :return: The web service root
+ :rtype: `Launchpad`
+ """
+ credentials = Credentials(consumer_name)
+ 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())
+ authorization_url = credentials.get_request_token(web_root=web_root)
+ webbrowser.open(authorization_url)
+ print "The authorization page:"
+ print " (%s)" % authorization_url
+ print "should be opening in your browser. After you have authorized"
+ print "this program to access Launchpad on your behalf you should come"
+ print "back here and press <Enter> to finish the authentication process."
+ sys.stdin.readline()
+ credentials.exchange_request_token_for_access_token(web_root)
+ return cls(credentials, service_root, cache, timeout, proxy_info)
+
+ @classmethod
+ def login_with(cls, consumer_name,
+ service_root=STAGING_SERVICE_ROOT,
+ launchpadlib_dir=None, timeout=None, proxy_info=None):
+ """Log in to Launchpad with possibly cached credentials.
+
+ This is a convenience method for either setting up new login
+ credentials, or re-using existing ones. When a login token is
+ generated using this method, the resulting credentials will be
+ saved in the `launchpadlib_dir` directory. If the same
+ `launchpadlib_dir` is passed in a second time, the credentials
+ in `launchpadlib_dir` for the consumer will be used
+ automatically.
+
+ Each consumer has their own credentials per service root in
+ `launchpadlib_dir`. `launchpadlib_dir` is also used for caching
+ fetched objects. The cache is per service root, and shared by
+ all consumers.
+
+ See `Launchpad.get_token_and_login()` for more information about
+ how new tokens are generated.
+
+ :param consumer_name: The consumer name, as appropriate for the
+ `Consumer` constructor
+ :type consumer_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 where the cache and
+ credentials are stored.
+ :type launchpadlib_dir: string
+ :return: The web service root
+ :rtype: `Launchpad`
+
+ """
+ if launchpadlib_dir is None:
+ home_dir = os.environ['HOME']
+ launchpadlib_dir = os.path.join(home_dir, '.launchpadlib')
+ launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
+ # Determine the real service root.
+ if service_root in cls.service_roots:
+ service_root = cls.service_roots[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)
+ credentials_path = os.path.join(service_root_dir, 'credentials')
+ if not os.path.exists(credentials_path):
+ os.makedirs(credentials_path)
+ consumer_credentials_path = os.path.join(
+ credentials_path, consumer_name)
+ if os.path.exists(consumer_credentials_path):
+ credentials = Credentials.load_from_path(
+ consumer_credentials_path)
+ launchpad = cls(
+ credentials, service_root=service_root, cache=cache_path,
+ timeout=timeout, proxy_info=proxy_info)
+ else:
+ launchpad = cls.get_token_and_login(
+ consumer_name, service_root=service_root, cache=cache_path,
+ timeout=timeout, proxy_info=proxy_info)
+ launchpad.credentials.save_to_path(
+ os.path.join(credentials_path, consumer_name))
+ os.chmod(
+ os.path.join(credentials_path, consumer_name),
+ stat.S_IREAD | stat.S_IWRITE)
+ return launchpad
+
+
+class OAuthSigningHttp(RestfulHttp):
+ """A client that signs every outgoing request with OAuth credentials."""
+
+ def _request(self, conn, host, absolute_uri, request_uri, method, body,
+ headers, redirections, cachekey):
+ """Sign a request with OAuth credentials before sending it."""
+ oauth_request = OAuthRequest.from_consumer_and_token(
+ self.restful_credentials.consumer,
+ self.restful_credentials.access_token,
+ http_url=absolute_uri)
+ oauth_request.sign_request(
+ OAuthSignatureMethod_PLAINTEXT(),
+ self.restful_credentials.consumer,
+ self.restful_credentials.access_token)
+ if headers.has_key('authorization'):
+ # There's an authorization header left over from a
+ # previous request that resulted in a redirect. Remove it
+ # and start again.
+ del headers['authorization']
+ headers.update(oauth_request.to_header(OAUTH_REALM))
+ return super(OAuthSigningHttp, self)._request(
+ conn, host, absolute_uri, request_uri, method, body, headers,
+ redirections, cachekey)
diff --git a/src/launchpadlib/testing/__init__.py b/src/launchpadlib/testing/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/launchpadlib/testing/__init__.py
diff --git a/src/launchpadlib/testing/helpers.py b/src/launchpadlib/testing/helpers.py
new file mode 100644
index 0000000..4933907
--- /dev/null
+++ b/src/launchpadlib/testing/helpers.py
@@ -0,0 +1,63 @@
+# 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
+# <http://www.gnu.org/licenses/>.
+
+"""launchpadlib testing helpers."""
+
+
+__metaclass__ = type
+__all__ = [
+ 'TestableLaunchpad',
+ 'nopriv_read_nonprivate',
+ 'salgado_read_nonprivate',
+ 'salgado_with_full_permissions',
+ ]
+
+
+from launchpadlib.launchpad import Launchpad
+
+
+class TestableLaunchpad(Launchpad):
+ """A base class for talking to the testing root service."""
+
+ # Use our test service root.
+ TESTING_SERVICE_ROOT = 'http://api.launchpad.dev:8085/beta/'
+
+ def __init__(self, credentials, service_root_ignored=None,
+ cache=None, timeout=None, proxy_info=None):
+ super(TestableLaunchpad, self).__init__(
+ credentials, TestableLaunchpad.TESTING_SERVICE_ROOT,
+ cache, timeout, proxy_info)
+
+
+class KnownTokens:
+ """Known access token/secret combinations."""
+
+ def __init__(self, token_string, access_secret):
+ self.token_string = token_string
+ self.access_secret = access_secret
+
+ def login(self, cache=None, timeout=None, proxy_info=None):
+ """Login using these credentials."""
+ return TestableLaunchpad.login(
+ 'launchpad-library', self.token_string, self.access_secret,
+ cache=cache, timeout=timeout, proxy_info=proxy_info)
+
+
+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')
diff --git a/src/launchpadlib/tests/__init__.py b/src/launchpadlib/tests/__init__.py
new file mode 100644
index 0000000..786a83f
--- /dev/null
+++ b/src/launchpadlib/tests/__init__.py
@@ -0,0 +1,16 @@
+# 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 <http://www.gnu.org/licenses/>.
+"""Tests for launchpadlib"""
diff --git a/src/launchpadlib/tests/test_credentials.py b/src/launchpadlib/tests/test_credentials.py
new file mode 100644
index 0000000..c14e823
--- /dev/null
+++ b/src/launchpadlib/tests/test_credentials.py
@@ -0,0 +1,60 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Tests for the Credentials class."""
+
+__metaclass__ = type
+
+
+import os
+import os.path
+import shutil
+import tempfile
+import unittest
+
+from launchpadlib.credentials import AccessToken, Credentials
+
+
+class TestCredentialsSaveAndLoad(unittest.TestCase):
+ """Test for saving and loading credentials."""
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self.temp_dir)
+
+ def test_save_to_and_load_from__path(self):
+ # Credentials can be saved to and loaded from a file using
+ # save_to_path() and load_from_path().
+ credentials_path = os.path.join(self.temp_dir, 'credentials')
+ credentials = Credentials(
+ 'consumer.key', consumer_secret='consumer.secret',
+ access_token=AccessToken('access.key', 'access.secret'))
+ credentials.save_to_path(credentials_path)
+ self.assertTrue(os.path.exists(credentials_path))
+
+ loaded_credentials = Credentials.load_from_path(credentials_path)
+ self.assertEqual(loaded_credentials.consumer.key, 'consumer.key')
+ self.assertEqual(
+ loaded_credentials.consumer.secret, 'consumer.secret')
+ self.assertEqual(
+ loaded_credentials.access_token.key, 'access.key')
+ self.assertEqual(
+ loaded_credentials.access_token.secret, 'access.secret')
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
diff --git a/src/launchpadlib/tests/test_launchpad.py b/src/launchpadlib/tests/test_launchpad.py
new file mode 100644
index 0000000..a7787d0
--- /dev/null
+++ b/src/launchpadlib/tests/test_launchpad.py
@@ -0,0 +1,230 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Tests for the Launchpad class."""
+
+__metaclass__ = type
+
+import os
+import shutil
+import stat
+import tempfile
+import unittest
+
+from launchpadlib.credentials import AccessToken, Credentials
+from launchpadlib.launchpad import Launchpad
+
+
+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.
+ """
+
+ consumer_name = None
+ passed_in_kwargs = None
+ credentials = None
+ get_token_and_login_called = False
+ service_roots = dict(example='http://api.example.com/beta')
+
+ def __init__(self, credentials, **kw):
+ self.credentials = credentials
+ self.passed_in_kwargs = kw
+
+ @classmethod
+ def get_token_and_login(cls, consumer_name, **kw):
+ """Create fake credentials and record that we were called."""
+ credentials = Credentials(
+ consumer_name, consumer_secret='consumer_secret:42',
+ access_token=AccessToken('access_key:84', 'access_secret:168'))
+ launchpad = cls(credentials, **kw)
+ launchpad.get_token_and_login_called = True
+ launchpad.consumer_name = consumer_name
+ launchpad.passed_in_kwargs = kw
+ return launchpad
+
+
+class TestLaunchpadLoginWith(unittest.TestCase):
+ """Tests for Launchpad.login_with()."""
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self.temp_dir)
+
+ def test_dirs_created(self):
+ # The path we pass into login_with() is the directory where
+ # cache and credentials for all service roots are stored.
+ launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
+ launchpad = NoNetworkLaunchpad.login_with(
+ 'not important', service_root='http://api.example.com/beta',
+ 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' and a
+ # 'credentials' directory.
+ self.assertTrue(
+ os.path.isdir(os.path.join(service_path, 'cache')))
+ credentials_path = os.path.join(service_path, 'credentials')
+ self.assertTrue(os.path.isdir(credentials_path))
+
+ def test_no_credentials_calls_get_token_and_login(self):
+ # If no credentials are found, get_token_and_login() is called.
+ service_root = 'http://api.example.com/beta'
+ 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)
+ self.assertEqual(launchpad.consumer_name, 'app name')
+ expected_arguments = dict(
+ service_root=service_root,
+ timeout=timeout,
+ proxy_info=proxy_info,
+ cache=os.path.join(self.temp_dir, 'api.example.com', 'cache'))
+ self.assertEqual(launchpad.passed_in_kwargs, expected_arguments)
+
+ def test_new_credentials_are_saved(self):
+ # After get_token_and_login() have been called, the created
+ # credentials are saved.
+ launchpad = NoNetworkLaunchpad.login_with(
+ 'app name', launchpadlib_dir=self.temp_dir,
+ service_root='http://api.example.com/beta')
+ credentials_path = os.path.join(
+ self.temp_dir, 'api.example.com', 'credentials', 'app name')
+ self.assertTrue(os.path.exists(credentials_path))
+ # Make sure that the credentials can be loaded, thus were
+ # written correctly.
+ loaded_credentials = Credentials.load_from_path(credentials_path)
+ self.assertEqual(loaded_credentials.consumer.key, 'app name')
+ self.assertEqual(
+ loaded_credentials.consumer.secret, 'consumer_secret:42')
+ self.assertEqual(
+ loaded_credentials.access_token.key, 'access_key:84')
+ self.assertEqual(
+ loaded_credentials.access_token.secret, 'access_secret:168')
+
+ def test_new_credentials_are_secure(self):
+ # The newly created credentials file is only readable and
+ # writable by the user.
+ launchpad = NoNetworkLaunchpad.login_with(
+ 'app name', launchpadlib_dir=self.temp_dir,
+ service_root='http://api.example.com/beta')
+ credentials_path = os.path.join(
+ self.temp_dir, 'api.example.com', 'credentials', 'app name')
+ statinfo = os.stat(credentials_path)
+ mode = stat.S_IMODE(statinfo.st_mode)
+ self.assertEqual(mode, stat.S_IWRITE | stat.S_IREAD)
+
+ def test_existing_credentials_are_reused(self):
+ # If a credential file for the application already exists, that
+ # one is used.
+ 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)
+
+ launchpad = NoNetworkLaunchpad.login_with(
+ 'app name', launchpadlib_dir=self.temp_dir,
+ service_root='http://api.example.com/beta')
+ self.assertFalse(launchpad.get_token_and_login_called)
+ self.assertEqual(launchpad.credentials.consumer.key, 'app name')
+ self.assertEqual(
+ launchpad.credentials.consumer.secret, 'consumer_secret:42')
+ self.assertEqual(
+ launchpad.credentials.access_token.key, 'access_key:84')
+ self.assertEqual(
+ launchpad.credentials.access_token.secret, 'access_secret:168')
+
+ 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)
+
+ service_root = 'http://api.example.com/beta'
+ 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)
+ expected_arguments = dict(
+ service_root=service_root,
+ timeout=timeout,
+ proxy_info=proxy_info,
+ cache=os.path.join(self.temp_dir, 'api.example.com', 'cache'))
+ self.assertEqual(launchpad.passed_in_kwargs, expected_arguments)
+
+ 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='http://api.example.com/beta')
+ # Reset the environment to the old value.
+ os.environ['HOME'] = old_home
+
+ cache_dir = launchpad.passed_in_kwargs['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')))
+ self.assertTrue(os.path.exists(
+ os.path.join(launchpadlib_dir, 'api.example.com', 'credentials')))
+
+ def test_short_service_name(self):
+ # A short service name is converted to the full service root URL.
+ launchpad = NoNetworkLaunchpad.login_with('app name', 'example')
+ self.assertEqual(
+ launchpad.passed_in_kwargs['service_root'],
+ 'http://api.example.com/beta')
+
+ def test_short_service_name_bad(self):
+ # A short service name that does not match one of the pre-defined
+ # service root names is passed through.
+ launchpad = NoNetworkLaunchpad.login_with('app name', 'foo')
+ self.assertEqual(
+ launchpad.passed_in_kwargs['service_root'],
+ 'foo')
+
+ def test_short_service_names(self):
+ # Ensure the short service names are all supported.
+ expected = ['production', 'edge', 'staging', 'dogfood', 'dev']
+ self.assertEqual(
+ sorted(Launchpad.service_roots.keys()),
+ sorted(expected))
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
diff --git a/src/launchpadlib/wadl-to-refhtml.xsl b/src/launchpadlib/wadl-to-refhtml.xsl
new file mode 100644
index 0000000..60feba2
--- /dev/null
+++ b/src/launchpadlib/wadl-to-refhtml.xsl
@@ -0,0 +1,1045 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ wadl-to-refhtml.xsl
+
+ Generate HTML documentation for a webservice described in a WADL file.
+ This is tailored to WADL generated by Launchpad's web service.
+
+ Based on wadl_documentaion.xsl from Mark Nottingham <mnot@yahoo-inc.com>
+ that can be found at http://www.mnot.net/webdesc/
+ Copyright (c) 2006-2007 Yahoo! Inc.
+ Copyright (c) 2008 Canonical Ltd.
+
+ This work is licensed under the Creative Commons Attribution-ShareAlike 2.5
+ License. To view a copy of this license, visit
+ http://creativecommons.org/licenses/by-sa/2.5/
+ or send a letter to
+ Creative Commons
+ 543 Howard Street, 5th Floor
+ San Francisco, California, 94105, USA
+-->
+
+<xsl:stylesheet
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
+ xmlns:wadl="http://research.sun.com/wadl/2006/10"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.w3.org/1999/xhtml"
+ exclude-result-prefixes="xsl wadl html"
+>
+ <xsl:output
+ method="xml"
+ encoding="UTF-8"
+ indent="yes"
+ doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"
+ doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
+ />
+
+
+ <!-- Allow using key('id', 'people') to identify unique elements, since
+ the document doesn't have a parsed DTD.
+ -->
+ <xsl:key name="id" match="*[@id]" use="@id"/>
+
+ <!-- Embedded stylesheet. -->
+ <xsl:template name="css-stylesheet">
+ <style type="text/css">
+ body {
+ font-family: sans-serif;
+ font-size: 0.85em;
+ margin: 2em 8em;
+ }
+ .methods {
+ background-color: #eef;
+ padding: 1em;
+ margin-bottom: 0.5em;
+ }
+ .method {
+ padding-left: 4em;
+ }
+ h1 {
+ font-size: 2.5em;
+ }
+ h2 {
+ border-bottom: 1px solid black;
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+ font-size: 2em;
+ }
+ h3 {
+ color: orange;
+ font-size: 1.75em;
+ margin-top: 1.25em;
+ margin-bottom: 0em;
+ }
+ h4 {
+ font-size: 1.50em;
+ margin: 0em;
+ padding: 0em;
+ border-bottom: 2px solid white;
+ }
+ h5 {
+ font-size: 1.25em;
+ margin-left: -3em;
+ }
+ h6 {
+ font-size: 1.1em;
+ color: #99a;
+ margin: 0.5em 0em 0.25em 0em;
+ }
+ dd {
+ margin-left: 1em;
+ }
+ tt, code {
+ font-size: 1.2em;
+ }
+ table {
+ margin-bottom: 0.5em;
+ }
+ th {
+ text-align: left;
+ font-weight: normal;
+ color: black;
+ border-bottom: 1px solid black;
+ padding: 3px 6px;
+ }
+ td {
+ padding: 3px 6px;
+ vertical-align: top;
+ background-color: #f6f6ff;
+ font-size: 0.85em;
+ }
+ td p {
+ margin: 0px;
+ }
+ ul {
+ padding-left: 1.75em;
+ }
+ p + ul, p + ol, p + dl {
+ margin-top: 0em;
+ }
+ label {
+ font-weight: bold;
+ }
+ .optional {
+ font-weight: normal;
+ opacity: 0.75;
+ }
+ .toc-link {
+ font-size: 0.85em;
+ }
+ </style>
+ </xsl:template>
+
+ <!-- Contains the base URL for the webservice without a trailing
+ slash. -->
+ <xsl:variable name="base">
+ <xsl:variable name="uri" select="//wadl:resources/@base"/>
+ <xsl:choose>
+ <xsl:when
+ test="substring($uri, string-length($uri) , 1) = '/'">
+ <xsl:value-of
+ select="substring($uri, 1, string-length($uri) - 1)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="$uri"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+
+ <!-- Generate the URL to the top-level collection. -->
+ <xsl:template name="resource-uri-doc">
+ <xsl:param name="url"><xsl:value-of
+ select="$base"/>/<xsl:value-of select="@id"/></xsl:param>
+ <p><label>URL:</label>
+ <code><xsl:copy-of select="$url" /></code></p>
+ </xsl:template>
+
+ <xsl:template name="entry-uri-doc">
+ <xsl:call-template name="resource-uri-doc">
+ <xsl:with-param name="url">
+ <xsl:choose>
+ <xsl:when test="@id = 'has_milestones'
+ or @id = 'bug_target'
+ or @id = 'has_bugs'">
+ <em>depends on the underlying entry</em>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:call-template name="find-entry-uri"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:template>
+
+ <xsl:template name="find-entry-uri">
+ <xsl:value-of select="$base"/>
+ <xsl:choose>
+ <xsl:when test="@id = 'archive'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution&gt;</var>
+ <xsl:text>/+archive/</xsl:text>
+ <var>&lt;archive.name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'archive_permission'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;archive.distribution&gt;</var>
+ <xsl:text>/+archive/</xsl:text>
+ <var>&lt;archive.name&gt;</var>
+ <xsl:text>/+</xsl:text>
+ <xsl:text>name</xsl:text>
+ <xsl:text>/</xsl:text>
+ <xsl:text>person.name</xsl:text>
+ <xsl:text>.</xsl:text>
+ <xsl:text>[component or source package].name</xsl:text>
+ </xsl:when>
+ <xsl:when test="@id = 'binary_package_publishing_history'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution.name&gt;</var>
+ <xsl:text>/+archive/</xsl:text>
+ <var>&lt;binary_package.name&gt;</var>
+ <xsl:text>/+binarypub/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'branch'">
+ <xsl:text>/~</xsl:text>
+ <var>&lt;author.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;project.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'branch_merge_proposal'">
+ <xsl:text>/~</xsl:text>
+ <var>&lt;author.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;project.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;branch.name&gt;</var>
+ <xsl:text>/+merge/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'bug'">
+ <xsl:text>/bugs/</xsl:text><var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'bug_attachment'">
+ <xsl:text>/bugs/</xsl:text>
+ <var>&lt;bug.id&gt;</var>
+ <xsl:text>/attachments/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'bug_subscription'">
+ <xsl:text>/bugs/</xsl:text>
+ <var>&lt;bug.id&gt;</var>
+ <xsl:text>/subscriptions/</xsl:text>
+ <var>&lt;subscriber.name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'bug_task'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;target.name&gt;</var>
+ <xsl:text>/+bug/</xsl:text>
+ <var >&lt;bug.id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'bug_watch'">
+ <xsl:text>/bugs/</xsl:text>
+ <var>&lt;bug.id&gt;</var>
+ <xsl:text>/watch/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'bug_tracker'">
+ <xsl:text>/bugs/bugtrackers/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'build'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution.name&gt;</var>
+ <xsl:text>/+source/</xsl:text>
+ <var>&lt;source_package.name&gt;</var>
+ <xsl:text>/+build/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'cve'">
+ <xsl:text>/bugs/cve/</xsl:text>
+ <var>&lt;sequence&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'distribution_source_package'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution.name&gt;</var>
+ <xsl:text>/+source/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'distro_arch_series'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;distroseries.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;architecture_tag&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'distro_series'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'email_address'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;person.name&gt;</var>
+ <xsl:text>/+email/</xsl:text>
+ <var>&lt;email&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'h_w_device'">
+ <xsl:text>/+hwdb/+device/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'h_w_device_class'">
+ <xsl:text>/+hwdb/+deviceclass/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'h_w_driver'">
+ <xsl:text>/+hwdb/+driver/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'h_w_submission'">
+ <xsl:text>/+hwdb/+submission/</xsl:text>
+ <var>&lt;submission-key&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'h_w_submission_device'">
+ <xsl:text>/+hwdb/+submissiondevice/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'h_w_vendor_i_d'">
+ <xsl:text>/+hwdb/+hwvendorid/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'jabber_id'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;person.name&gt;</var>
+ <xsl:text>/+jabberid/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'irc_id'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;person.name&gt;</var>
+ <xsl:text>/+ircnick/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'language'">
+ <xsl:text>/+languages/</xsl:text>
+ <var>&lt;code&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'message'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;target.name&gt;</var>
+ <xsl:text>/+bug/</xsl:text>
+ <var>&lt;bug.id&gt;</var>
+ <xsl:text>/comments/</xsl:text>
+ <var>&lt;index&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'milestone'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;target.name&gt;</var>
+ <xsl:text>/+milestone/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test=" @id = 'distribution'
+ or @id = 'pillar'
+ or @id = 'product'
+ or @id = 'project'
+ or @id = 'project_group'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'team' or @id = 'person'">
+ <xsl:text>/~</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'product_release'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;product.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;product_series.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'product_series'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;product.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'project_release'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;project.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;project_series.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;release.version&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'project_release_file'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;project.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;project_series.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;release.version&gt;</var>
+ <xsl:text>/+file/</xsl:text>
+ <var>&lt;hosted_file.filename&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'project_series'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;project.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'source_package'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution.name&gt;</var>
+ <xsl:text>/</xsl:text>
+ <var>&lt;distro_series.name&gt;</var>
+ <xsl:text>/+source/</xsl:text>
+ <var>&lt;name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'source_package_publishing_history'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;distribution&gt;</var>
+ <xsl:text>/+archive/</xsl:text>
+ <var>&lt;name&gt;</var>
+ <xsl:text>/+sourcepub/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'team_membership'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;team.name&gt;</var>
+ <xsl:text>/+member/</xsl:text>
+ <var>&lt;member.name&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'wiki_name'">
+ <xsl:text>/</xsl:text>
+ <var>&lt;person.name&gt;</var>
+ <xsl:text>/+wikiname/</xsl:text>
+ <var>&lt;id&gt;</var>
+ </xsl:when>
+ <xsl:when test="@id = 'commercial_subscription'">
+ <xsl:text>/+commercialsubscription/</xsl:text>
+ <var>&lt;commercial_subscription.id&gt;</var>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:message>Unknown entry URL:
+ <xsl:value-of select="@id" />
+ </xsl:message>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+ <!-- We start here. -->
+ <xsl:template match="/wadl:application">
+ <xsl:variable name="title">
+ <xsl:choose>
+ <xsl:when test="wadl:doc[@title]">
+ <xsl:value-of select="wadl:doc[@title][1]/@title"/>
+ </xsl:when>
+ <xsl:otherwise>Launchpad Web Service API</xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+ <html>
+ <head>
+ <title><xsl:value-of select="$title" /></title>
+ <xsl:call-template name="css-stylesheet"/>
+ </head>
+ <body>
+ <h1><xsl:value-of select="$title" /></h1>
+ <xsl:apply-templates select="wadl:doc"/>
+
+ <xsl:call-template name="table-of-contents" />
+ <xsl:call-template name="top-level-collections" />
+ <xsl:call-template name="entry-types" />
+ </body>
+ </html>
+ </xsl:template>
+
+ <!-- Table of contents -->
+ <xsl:template name="table-of-contents">
+ <div id="toc">
+ <h2>Table of Contents</h2>
+ <h3>Top-level collections</h3>
+ <ul>
+ <xsl:for-each
+ select="key('id', 'service-root-json')/wadl:param/wadl:link">
+ <xsl:sort select="../@name" />
+ <xsl:variable name="collection_id"
+ select="substring-after(@resource_type, '#')" />
+ <xsl:if test="string-length($collection_id) &gt; 0">
+ <li><a href="#{$collection_id}">
+ <xsl:call-template name="get-title-or-id">
+ <xsl:with-param name="element" select="key('id', $collection_id)" />
+ </xsl:call-template>
+ </a></li>
+ </xsl:if>
+ </xsl:for-each>
+ </ul>
+ <h3>Entry types</h3>
+ <ul>
+ <xsl:for-each select="wadl:resource_type[
+ @id != 'service-root'
+ and @id != 'HostedFile'
+ and not(contains(@id, 'page-resource'))
+ ]">
+ <xsl:sort select="@id" />
+ <xsl:variable name="id" select="./@id"/>
+ <xsl:variable name="top_level_collections"
+ select="key('id', 'service-root-json')//@resource_type[
+ substring-after(., '#') = $id]" />
+ <xsl:if test="not($top_level_collections[contains(., $id)])">
+ <li><a href="#{$id}">
+ <xsl:call-template name="get-title-or-id">
+ <xsl:with-param name="element" select="." />
+ </xsl:call-template>
+ </a></li>
+ </xsl:if>
+ </xsl:for-each>
+ </ul>
+ </div>
+ </xsl:template>
+
+ <!-- Top level collections container -->
+ <xsl:template name="top-level-collections">
+ <div id="top-level-collections">
+ <h2>Top-level collections</h2>
+
+ <!--
+ Top-level collections are found in the WADL by
+ looking at the representation of the service-root resource
+ and processing all the resource-type linked from it.
+ -->
+ <xsl:for-each
+ select="key('id', 'service-root-json')/wadl:param/wadl:link">
+ <xsl:sort select="../@name" />
+ <xsl:variable name="collection_id"
+ select="substring-after(@resource_type, '#')" />
+
+ <xsl:apply-templates
+ select="key('id', $collection_id)"
+ mode="top-level-collections" />
+ </xsl:for-each>
+ </div>
+ </xsl:template>
+
+ <xsl:template name="find-root-object-uri">
+ <xsl:value-of select="$base"/>
+ <xsl:choose>
+ <xsl:when test="@id = 'hwdb'">
+ <xsl:text>/+hwdb</xsl:text>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:text>/</xsl:text><xsl:value-of select="@id" />
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Documentation for one top-level-collection -->
+ <xsl:template match="wadl:resource_type" mode="top-level-collections">
+ <div id="{@id}" class="top-level-collection">
+ <h3><xsl:call-template name="get-title-or-id"/></h3>
+ <xsl:apply-templates select="wadl:doc"/>
+
+ <xsl:call-template name="resource-uri-doc">
+ <xsl:with-param name="url">
+ <!-- The default URL schema used for root objects of
+ Launchpad's webservice is
+
+ [urlbase]/[root-object-name]
+
+ e.g,
+
+ https://api.launchpad.net/beta/bugs
+
+ while the HWDB application root's URL is
+
+ https://api.launchpad.net/beta/+hwdb
+
+ In other words, the URL for the HWDB application
+ root needs to be mangled in a form similar to
+ that used for non-root objects in the template
+ "find-entry-uri".
+ -->
+
+ <xsl:call-template name="find-root-object-uri"/>
+ </xsl:with-param>
+ </xsl:call-template>
+
+ <!-- All top-level collections supports a GET without arguments
+ iterating over all the resources.
+ The type of the resource is found by looking at the href attribute
+ of the default representation. Link is in the form
+ <resource>-page.
+ -->
+ <div class="methods standard">
+ <h4>Standard method</h4>
+ <xsl:variable name="default_get"
+ select="wadl:method[not(wadl:request)][1]" />
+ <xsl:variable name="resource_type"
+ select="substring-after(
+ substring-before(
+ $default_get//wadl:representation[
+ not(@mediaType)]/@href, '-page'),
+ '#')" />
+ <dl>
+ <dt>GET</dt>
+ <dd>Response contains a <a href="#{$resource_type}"
+ ><xsl:call-template name="get-title-or-id">
+ <xsl:with-param name="element"
+ select="key('id', $resource_type)" />
+ </xsl:call-template></a>
+ collection.</dd>
+ </dl>
+ </div>
+
+ <xsl:call-template name="custom-GETs" />
+ <xsl:call-template name="custom-POSTs" />
+ <a href="#toc" class="toc-link">(back to Table of Contents)</a>
+ </div>
+ </xsl:template>
+
+ <!-- Documentation for the standard methods on an entry -->
+ <xsl:template name="standard-methods">
+ <div id="{@id}-standard-methods" class="methods standard">
+ <h4>Standard methods</h4>
+ <dl>
+ <!-- Standard methods are the ones without a ws.op param. -->
+ <xsl:apply-templates
+ select="wadl:method[not(.//wadl:param[@name = 'ws.op'])]"
+ mode="standard-method">
+ <xsl:sort select="@name"/>
+ </xsl:apply-templates>
+ </dl>
+ </div>
+ </xsl:template>
+
+ <!-- Documentation for the standard GET on an entry -->
+ <xsl:template match="wadl:method[@name='GET']" mode="standard-method">
+ <dt><xsl:value-of select="@name" /></dt>
+ <dd>Response contains the default
+ <xsl:call-template name="representation-type" /> representation
+ for this entry.
+ </dd>
+ </xsl:template>
+
+ <!-- Documentation for the standard PUT on an entry -->
+ <xsl:template match="wadl:method[@name='PUT']" mode="standard-method">
+ <dt><xsl:value-of select="@name" /></dt>
+ <dd>Entity body should contain a representation encoded using
+ <xsl:call-template name="representation-type" /> of the entry.
+ All fields of the default representation should be included. Only
+ fields marked as writeable in the default representation should be
+ modified.
+ </dd>
+ </xsl:template>
+
+ <!-- Documentation for the standard PATCH on an entry -->
+ <xsl:template match="wadl:method[@name='PATCH']" mode="standard-method">
+ <dt><xsl:value-of select="@name" /></dt>
+ <dd>Entity body should contain a represention encoded using
+ <xsl:call-template name="representation-type"/> of the entry
+ fields to update. Any fields of the default representation marked
+ as writeable can be included.
+ </dd>
+ </xsl:template>
+
+ <!-- Documentation for the custom GET operations of the resource type -->
+ <xsl:template name="custom-GETs">
+ <xsl:variable name="operations" select="wadl:method[
+ @name = 'GET'][.//wadl:param[@name = 'ws.op']]" />
+
+ <xsl:if test="$operations">
+ <div id="{@id}-custom-GETs" class="methods GETs">
+ <h4>Custom GET methods</h4>
+
+ <xsl:apply-templates select="$operations">
+ <xsl:sort select=".//wadl:param[@name='ws.op']/@fixed"/>
+ </xsl:apply-templates>
+ </div>
+ </xsl:if>
+ </xsl:template>
+
+ <!-- Documentation for the custom POST operations of the resource type -->
+ <xsl:template name="custom-POSTs">
+ <xsl:variable name="operations" select="wadl:method[
+ @name = 'POST'][.//wadl:param[@name = 'ws.op']]" />
+
+ <xsl:if test="$operations">
+ <div id="{@id}-custom-POSTs" class="methods POSTs">
+ <h4>Custom POST methods</h4>
+
+ <xsl:apply-templates select="$operations">
+ <xsl:sort select=".//wadl:param[@name='ws.op']/@fixed"/>
+ </xsl:apply-templates>
+ </div>
+ </xsl:if>
+ </xsl:template>
+
+ <!-- Container for all the entry types documentation -->
+ <xsl:template name="entry-types">
+ <h2 id="entry-types">Entry types</h2>
+
+ <!-- Process all the resource_types, except the service-root ones,
+ the type describing collections of that type,
+ or any other ones, linked from within the service root.
+ -->
+ <xsl:for-each select="wadl:resource_type[
+ @id != 'service-root'
+ and @id != 'HostedFile'
+ and not(contains(@id, 'page-resource'))
+ ]">
+ <xsl:sort select="@id" />
+ <xsl:variable name="id" select="./@id"/>
+ <xsl:variable name="top_level_collections"
+ select="key('id', 'service-root-json')//@resource_type[
+ substring-after(., '#') = $id]" />
+ <xsl:if test="not($top_level_collections[contains(., $id)])">
+ <xsl:apply-templates select="." mode="entry-types" />
+ </xsl:if>
+ </xsl:for-each>
+ </xsl:template>
+
+ <!-- Documentation for one entry-type -->
+ <xsl:template match="wadl:resource_type" mode="entry-types">
+ <h3 id="{@id}"><xsl:call-template name="get-title-or-id"/></h3>
+ <xsl:apply-templates select="wadl:doc"/>
+
+ <xsl:call-template name="entry-uri-doc"/>
+
+ <xsl:call-template name="default-representation" />
+ <xsl:call-template name="standard-methods" />
+ <xsl:call-template name="custom-GETs" />
+ <xsl:call-template name="custom-POSTs" />
+ <a href="#toc" class="toc-link">(back to Table of Contents)</a>
+ </xsl:template>
+
+ <!-- Documentation of the default representation for an entry -->
+ <xsl:template name="default-representation">
+ <xsl:variable name="default_get" select="wadl:method[
+ @name = 'GET' and not(wadl:request)]" />
+ <xsl:variable name="representation" select="key(
+ 'id', substring-after(
+ $default_get/wadl:response/wadl:representation[
+ not(@mediaType)]/@href, '#'))"/>
+
+ <div class="representation">
+ <h4>Default representation
+ (<xsl:value-of select="$representation/@mediaType"/>)</h4>
+
+ <table>
+ <tr>
+ <th>Key</th>
+ <th>Value</th>
+ <th>Description</th>
+ </tr>
+ <xsl:apply-templates select="$representation/wadl:param"
+ mode="representation">
+ <xsl:sort select="@name"/>
+ </xsl:apply-templates>
+ </table>
+ </div>
+ </xsl:template>
+
+ <!-- Output the cell containing the field name.
+
+ current() should be a wadl:param.
+ -->
+ <xsl:template name="param-name">
+ <td>
+ <p><strong><xsl:value-of select="@name"/></strong></p>
+ </td>
+ </xsl:template>
+
+ <!-- Output a table cell containing the parameter description.
+
+ current() should a wadl:param.
+ -->
+ <xsl:template name="param-description">
+ <td>
+ <xsl:apply-templates select="wadl:doc"/>
+ <xsl:if test="wadl:option[wadl:doc]">
+ <dl>
+ <xsl:apply-templates
+ select="wadl:option" mode="option-doc"/>
+ </dl>
+ </xsl:if>
+ </td>
+ </xsl:template>
+
+ <!-- Output information about the parameter value.
+
+ current() should be a wadl:param.
+ -->
+ <xsl:template name="param-value">
+ <xsl:if test="wadl:option">
+ <p><em>One of:</em></p>
+ <ul>
+ <xsl:apply-templates select="wadl:option"/>
+ </ul>
+ </xsl:if>
+ <xsl:apply-templates select="wadl:link[@resource_type]"/>
+ <xsl:if test="@default">
+ <p>
+ Default:
+ <var><xsl:value-of select="@default"/></var>
+ </p>
+ </xsl:if>
+ <xsl:if test="@fixed">
+ <p>
+ Fixed:
+ <var><xsl:value-of select="@fixed"/></var>
+ </p>
+ </xsl:if>
+ </xsl:template>
+
+ <!-- Output row describing one field in the default representation -->
+ <xsl:template match="wadl:param" mode="representation">
+ <xsl:variable name="resource_type"
+ select="substring-before(../@id, '-')" />
+ <xsl:variable name="patch_representation_id"
+ ><xsl:value-of select="$resource_type"/>-diff</xsl:variable>
+ <xsl:variable name="patch_representation"
+ select="key('id', $patch_representation_id)"/>
+ <tr>
+ <xsl:call-template name="param-name"/>
+ <td>
+ <p>
+ <xsl:choose>
+ <xsl:when test="$patch_representation/wadl:param[@name
+ = current()/@name]">
+ <small>(writeable)</small>
+ </xsl:when>
+ <xsl:otherwise>
+ <small>(read-only)</small>
+ </xsl:otherwise>
+ </xsl:choose>
+ </p>
+ <xsl:call-template name="param-value" />
+ </td>
+ <xsl:call-template name="param-description" />
+ </tr>
+ </xsl:template>
+
+ <!-- Output the description of a link type in param listing -->
+ <xsl:template match="wadl:link[
+ @resource_type and ../@name != 'self_link']">
+ <xsl:variable name="resource_type"
+ select="substring-after(@resource_type, '#')"/>
+ <xsl:choose>
+ <xsl:when test="contains($resource_type, 'page-resource')">
+ Link to a <a href="#{substring-before($resource_type, '-')}"
+ ><xsl:value-of
+ select="substring-before($resource_type, '-')"
+ /></a> collection.
+ </xsl:when>
+ <xsl:when test="$resource_type = 'HostedFile'">
+ Link to a file resource.
+ </xsl:when>
+ <xsl:otherwise>
+ Link to a <a href="#{$resource_type}"
+ ><xsl:value-of select="$resource_type"/></a>.
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Documentation for a custom method -->
+ <xsl:template match="wadl:method[.//wadl:param[@name = 'ws.op']]">
+ <div class="method">
+ <h5 id="{@id}"><xsl:value-of
+ select=".//wadl:param[@name = 'ws.op']/@fixed"/></h5>
+ <xsl:choose>
+ <xsl:when test="wadl:doc|wadl:request|wadl:response">
+ <xsl:apply-templates select="wadl:doc"/>
+ <xsl:apply-templates select="wadl:request"/>
+ <xsl:apply-templates select="wadl:response"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <p><em>Missing documentation.</em></p>
+ </xsl:otherwise>
+ </xsl:choose>
+ </div>
+ </xsl:template>
+
+ <!-- Documentation for the request parameters of a custom method -->
+ <xsl:template match="wadl:request">
+ <h6>Parameters</h6>
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Value</th>
+ <th>Description</th>
+ </tr>
+ <xsl:apply-templates
+ select=".//wadl:param[@style='query'][@fixed]"/>
+ <xsl:apply-templates
+ select=".//wadl:param[@style='query'][not(@fixed)]">
+ <xsl:sort select="@name" />
+ </xsl:apply-templates>
+ </table>
+ </xsl:template>
+
+ <!-- Documentation for the response of custom methods returning
+ and entry or a collection.
+ -->
+ <xsl:template match="wadl:response/wadl:representation[@href]">
+ <xsl:variable name="id" select="substring-after(@href, '#')" />
+ <xsl:variable name="resource_type"
+ select="substring-before($id, '-')"/>
+
+ <p class="response">Response contains an
+ <xsl:apply-templates select="key('id', $id)"
+ mode="representation-type"/>
+ representation of a
+ <a href="#{$resource_type}"><xsl:value-of
+ select="$resource_type"
+ /></a><xsl:if test="contains($id, '-page')">
+ collection
+ </xsl:if>.
+ </p>
+ </xsl:template>
+
+ <!-- Documentation for request parameter. -->
+ <xsl:template match="wadl:param">
+ <tr>
+ <xsl:call-template name="param-name"/>
+ <td>
+ <xsl:if test="@required or @repeating">
+ <p>
+ <xsl:if test="@required='true'">
+ <small>(required)</small>
+ </xsl:if>
+ <xsl:if test="@repeating='true'">
+ <small>(repeating)</small>
+ </xsl:if>
+ </p>
+ </xsl:if>
+ <xsl:call-template name="param-value"/>
+ </td>
+ <xsl:call-template name="param-description"/>
+ </tr>
+ </xsl:template>
+
+ <!-- Documentation for factories.
+
+ Factory's response include a Location header pointint to a resource type.
+ -->
+ <xsl:template match="wadl:response/wadl:param[
+ @name = 'Location' and @style = 'header'
+ and wadl:link[@resource_type]]">
+ <xsl:variable name="resource_type"
+ select="substring-after(
+ wadl:link[@resource_type]/@resource_type, '#')"/>
+ <p>On success, the response status will be 201 and the
+ <var>Location</var> header will contain the link to the newly
+ created <a href="#{$resource_type}"
+ ><xsl:value-of select="$resource_type" /></a>.
+ </p>
+ </xsl:template>
+
+ <!-- Output the available value for the parameter. -->
+ <xsl:template match="wadl:option">
+ <li>
+ <tt><xsl:value-of select="@value"/></tt>
+ <xsl:if test="ancestor::wadl:param[1]/@default=@value">
+ <small>(default)</small>
+ </xsl:if>
+ </li>
+ </xsl:template>
+
+ <!-- Ouput list of the documentation for each available option. -->
+ <xsl:template match="wadl:option" mode="option-doc">
+ <dt>
+ <tt><xsl:value-of select="@value"/></tt>
+ <xsl:if test="ancestor::wadl:param[1]/@default=@value">
+ <small>(default)</small>
+ </xsl:if>
+ </dt>
+ <dd>
+ <xsl:apply-templates select="wadl:doc"/>
+ </dd>
+ </xsl:template>
+
+ <!-- Format wadl:doc -->
+ <xsl:template match="wadl:doc">
+ <xsl:param name="inline">0</xsl:param>
+ <!-- skip WADL elements -->
+ <xsl:choose>
+ <xsl:when test="node()[1]=text() and $inline=0">
+ <p>
+ <xsl:apply-templates select="node()" mode="copy"/>
+ </p>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:apply-templates select="node()" mode="copy"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Returns the title or id of an element.
+
+ Look for the first wadl:doc title attribute content of the
+ current node or fall back to the element id.
+
+ :param element: The element to return the title or id. Defaults to the
+ current node.
+ -->
+ <xsl:template name="get-title-or-id">
+ <xsl:param name="element" select="current()" />
+ <xsl:choose>
+ <xsl:when test="$element/wadl:doc[@title]">
+ <xsl:value-of select="$element/wadl:doc[@title][1]/@title"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="$element/@id"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- Output the mediaType attribute of the default representation.
+
+ Should be call on an element that contain a wadl:representation element
+ without a mediaType attribute.
+ -->
+ <xsl:template name="representation-type">
+ <xsl:apply-templates
+ select="key('id',
+ substring-after(
+ .//wadl:representation[not(@mediaType)]/@href,
+ '#'))"
+ mode="representation-type"/>
+ </xsl:template>
+
+ <!-- Omit docutils parameter lists in methods since they are redundant
+ or misleading with the one we give. -->
+ <xsl:template match="wadl:method//html:table[
+ contains(@class, 'field-list')]"
+ mode="copy"/>
+
+ <!-- Output the mediaType attribute of a representation -->
+ <xsl:template match="wadl:representation[@mediaType]"
+ mode="representation-type">
+ <code><xsl:value-of select="@mediaType"/></code>
+ </xsl:template>
+
+ <!-- Copy html elements. -->
+ <xsl:template match="html:*" mode="copy">
+ <!-- remove the prefix on HTML elements -->
+ <xsl:element name="{local-name()}">
+ <xsl:for-each select="@*">
+ <xsl:attribute name="{local-name()}"
+ ><xsl:value-of select="."/></xsl:attribute>
+ </xsl:for-each>
+ <xsl:apply-templates select="node()" mode="copy"/>
+ </xsl:element>
+ </xsl:template>
+
+ <xsl:template match="@*|node()[
+ namespace-uri()!='http://www.w3.org/1999/xhtml']" mode="copy">
+ <!-- everything else goes straight through -->
+ <xsl:copy>
+ <xsl:apply-templates select="@*|node()" mode="copy"/>
+ </xsl:copy>
+ </xsl:template>
+
+</xsl:stylesheet>