[Checkins] SVN: z3c.rest/tags/0.2.4/ Release z3c.rest 0.2.4.

Marius Gedminas marius at pov.lt
Thu Sep 4 16:00:20 EDT 2008


Log message for revision 90829:
  Release z3c.rest 0.2.4.
  
  

Changed:
  A   z3c.rest/tags/0.2.4/
  D   z3c.rest/tags/0.2.4/CHANGES.txt
  A   z3c.rest/tags/0.2.4/CHANGES.txt
  D   z3c.rest/tags/0.2.4/setup.py
  A   z3c.rest/tags/0.2.4/setup.py
  D   z3c.rest/tags/0.2.4/src/z3c/rest/client.py
  A   z3c.rest/tags/0.2.4/src/z3c/rest/client.py
  D   z3c.rest/tags/0.2.4/src/z3c/rest/client.txt
  A   z3c.rest/tags/0.2.4/src/z3c/rest/client.txt
  D   z3c.rest/tags/0.2.4/src/z3c/rest/testing.py
  A   z3c.rest/tags/0.2.4/src/z3c/rest/testing.py

-=-
Copied: z3c.rest/tags/0.2.4 (from rev 90826, z3c.rest/trunk)

Deleted: z3c.rest/tags/0.2.4/CHANGES.txt
===================================================================
--- z3c.rest/trunk/CHANGES.txt	2008-09-04 19:24:13 UTC (rev 90826)
+++ z3c.rest/tags/0.2.4/CHANGES.txt	2008-09-04 20:00:20 UTC (rev 90829)
@@ -1,53 +0,0 @@
-=======
-CHANGES
-=======
-
-0.2.3 (2008-03-20)
-------------------
-
-- Bug/Misfeature: Sigh, getting the trailing slash handled correctly turned
-  out to be a big pain. I really hope I got it working the way it should be
-  for a REST client now.
-
-
-0.2.2 (2008-03-19)
-------------------
-
-- Bug/Misfeature: The client always added a slash to the end of the URL. But
-  some REST APIs are very sensitive to this. Now the slash is only preserved
-  if present, but nothing will be added otherwise.
-
-
-0.2.1 (2008-03-06)
-------------------
-
-- Bug: Sometimes the response body was not read and the contents of the client
-  was empty. Unfortunately, this problem could not be reliably reproduced, but
-  debugging showed that the connection was closed to early. (Roy Mathew)
-
-- Feature: Make the package Python 2.4 and 2.5 compatible.
-
-- Feature: Require lxml 2.0 for z3c.rest.
-
-
-0.2.0 (2008-03-03)
-------------------
-
-- Feature: Made the HTTP caller pluggable for the REST client, allowing
-  request types other than ``RESTRequest``.
-
-
-0.1.0 (2008-03-03)
-------------------
-
-- Initial Release
-
-  * Publisher hooks to build dedicated REST servers
-
-  * Error view support
-
-  * Pluggable REST traverser based on `z3c.traverser`
-
-  * REST client
-
-  * Minimal sample application

Copied: z3c.rest/tags/0.2.4/CHANGES.txt (from rev 90828, z3c.rest/trunk/CHANGES.txt)
===================================================================
--- z3c.rest/tags/0.2.4/CHANGES.txt	                        (rev 0)
+++ z3c.rest/tags/0.2.4/CHANGES.txt	2008-09-04 20:00:20 UTC (rev 90829)
@@ -0,0 +1,59 @@
+=======
+CHANGES
+=======
+
+0.2.4 (2008-09-04)
+------------------
+
+- RESTClient() now correctly interprets https:// URLs.
+
+
+0.2.3 (2008-03-20)
+------------------
+
+- Bug/Misfeature: Sigh, getting the trailing slash handled correctly turned
+  out to be a big pain. I really hope I got it working the way it should be
+  for a REST client now.
+
+
+0.2.2 (2008-03-19)
+------------------
+
+- Bug/Misfeature: The client always added a slash to the end of the URL. But
+  some REST APIs are very sensitive to this. Now the slash is only preserved
+  if present, but nothing will be added otherwise.
+
+
+0.2.1 (2008-03-06)
+------------------
+
+- Bug: Sometimes the response body was not read and the contents of the client
+  was empty. Unfortunately, this problem could not be reliably reproduced, but
+  debugging showed that the connection was closed to early. (Roy Mathew)
+
+- Feature: Make the package Python 2.4 and 2.5 compatible.
+
+- Feature: Require lxml 2.0 for z3c.rest.
+
+
+0.2.0 (2008-03-03)
+------------------
+
+- Feature: Made the HTTP caller pluggable for the REST client, allowing
+  request types other than ``RESTRequest``.
+
+
+0.1.0 (2008-03-03)
+------------------
+
+- Initial Release
+
+  * Publisher hooks to build dedicated REST servers
+
+  * Error view support
+
+  * Pluggable REST traverser based on `z3c.traverser`
+
+  * REST client
+
+  * Minimal sample application

Deleted: z3c.rest/tags/0.2.4/setup.py
===================================================================
--- z3c.rest/trunk/setup.py	2008-09-04 19:24:13 UTC (rev 90826)
+++ z3c.rest/tags/0.2.4/setup.py	2008-09-04 20:00:20 UTC (rev 90829)
@@ -1,91 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2007 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Setup
-
-$Id$
-"""
-import os
-from setuptools import setup, find_packages
-
-def read(*rnames):
-    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
-
-chapters = '\n'.join(
-    [read('src', 'z3c', 'rest', name)
-    for name in ('README.txt',
-                 'client.txt',
-                 'null.txt',
-                 'traverser.txt',
-                 'rest.txt')])
-
-setup (
-    name='z3c.rest',
-    version='0.2.4dev',
-    author = "Stephan Richter and the Zope Community",
-    author_email = "zope3-dev at zope.org",
-    description = "A REST Framework for Zope 3 Applications",
-    long_description=(
-        read('README.txt')
-        + '\n\n' +
-        'Detailed Documentation\n'
-        '**********************\n'
-        + '\n' + chapters
-        + '\n\n' +
-        read('CHANGES.txt')
-        ),
-    license = "ZPL 2.1",
-    keywords = "zope3 form widget",
-    classifiers = [
-        'Development Status :: 4 - Beta',
-        'Environment :: Web Environment',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: Zope Public License',
-        'Programming Language :: Python',
-        'Natural Language :: English',
-        'Operating System :: OS Independent',
-        'Topic :: Internet :: WWW/HTTP',
-        'Framework :: Zope3'],
-    url = 'http://cheeseshop.python.org/pypi/z3c.rest',
-    packages = find_packages('src'),
-    include_package_data = True,
-    package_dir = {'':'src'},
-    namespace_packages = ['z3c'],
-    extras_require = dict(
-        app = ['zope.app.appsetup',
-               'zope.app.authentication',
-               'zope.app.component',
-               'zope.app.container',
-               'zope.app.error',
-               'zope.app.form',
-               'zope.app.publisher',
-               'zope.app.publication',
-               'zope.app.security',
-               'zope.app.securitypolicy',
-               'zope.app.twisted',
-               'zope.app.wsgi',
-               'zope.app.zcmlfiles',
-               'zope.contentprovider',
-               ],
-        test = ['z3c.coverage',
-                'z3c.etestbrowser',
-                'zope.app.testing'],
-        ),
-    install_requires = [
-        'lxml>=2.0.0', # Changes in API, since 1.3.6.
-        'setuptools',
-        'z3c.traverser',
-        'zope.publisher',
-        ],
-    zip_safe = False,
-    )

Copied: z3c.rest/tags/0.2.4/setup.py (from rev 90828, z3c.rest/trunk/setup.py)
===================================================================
--- z3c.rest/tags/0.2.4/setup.py	                        (rev 0)
+++ z3c.rest/tags/0.2.4/setup.py	2008-09-04 20:00:20 UTC (rev 90829)
@@ -0,0 +1,91 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Setup
+
+$Id$
+"""
+import os
+from setuptools import setup, find_packages
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+chapters = '\n'.join(
+    [read('src', 'z3c', 'rest', name)
+    for name in ('README.txt',
+                 'client.txt',
+                 'null.txt',
+                 'traverser.txt',
+                 'rest.txt')])
+
+setup (
+    name='z3c.rest',
+    version='0.2.4',
+    author = "Stephan Richter and the Zope Community",
+    author_email = "zope3-dev at zope.org",
+    description = "A REST Framework for Zope 3 Applications",
+    long_description=(
+        read('README.txt')
+        + '\n\n' +
+        'Detailed Documentation\n'
+        '**********************\n'
+        + '\n' + chapters
+        + '\n\n' +
+        read('CHANGES.txt')
+        ),
+    license = "ZPL 2.1",
+    keywords = "zope3 form widget",
+    classifiers = [
+        'Development Status :: 4 - Beta',
+        'Environment :: Web Environment',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Zope Public License',
+        'Programming Language :: Python',
+        'Natural Language :: English',
+        'Operating System :: OS Independent',
+        'Topic :: Internet :: WWW/HTTP',
+        'Framework :: Zope3'],
+    url = 'http://cheeseshop.python.org/pypi/z3c.rest',
+    packages = find_packages('src'),
+    include_package_data = True,
+    package_dir = {'':'src'},
+    namespace_packages = ['z3c'],
+    extras_require = dict(
+        app = ['zope.app.appsetup',
+               'zope.app.authentication',
+               'zope.app.component',
+               'zope.app.container',
+               'zope.app.error',
+               'zope.app.form',
+               'zope.app.publisher',
+               'zope.app.publication',
+               'zope.app.security',
+               'zope.app.securitypolicy',
+               'zope.app.twisted',
+               'zope.app.wsgi',
+               'zope.app.zcmlfiles',
+               'zope.contentprovider',
+               ],
+        test = ['z3c.coverage',
+                'z3c.etestbrowser',
+                'zope.app.testing'],
+        ),
+    install_requires = [
+        'lxml>=2.0.0', # Changes in API, since 1.3.6.
+        'setuptools',
+        'z3c.traverser',
+        'zope.publisher',
+        ],
+    zip_safe = False,
+    )

Deleted: z3c.rest/tags/0.2.4/src/z3c/rest/client.py
===================================================================
--- z3c.rest/trunk/src/z3c/rest/client.py	2008-09-04 19:24:13 UTC (rev 90826)
+++ z3c.rest/tags/0.2.4/src/z3c/rest/client.py	2008-09-04 20:00:20 UTC (rev 90829)
@@ -1,208 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2007 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""REST Client
-
-$Id$
-"""
-import lxml
-import lxml.etree
-import httplib
-import socket
-import urllib
-import urlparse
-import base64
-import zope.interface
-from z3c.rest import interfaces
-
-def isRelativeURL(url):
-    """Determines whether the given URL is a relative path segment."""
-    pieces = urlparse.urlparse(url)
-    if not pieces[0] and not pieces[1]:
-        return True
-    return False
-
-def absoluteURL(base, url):
-    """Convertes a URL to an absolute URL given a base."""
-    if isRelativeURL(url):
-        if not base.endswith('/'):
-            base += '/'
-        fullUrl = urlparse.urljoin(base, url)
-    else:
-        fullUrl = url
-    pieces = list(urlparse.urlparse(fullUrl))
-    if not pieces[2].endswith('/'):
-        pieces[2] += '/'
-    newUrl = urlparse.urlunparse(pieces)
-    # Some systems really do not like the trailing /
-    if not url.endswith('/') and newUrl.endswith('/'):
-        newUrl = newUrl[:-1]
-    return newUrl
-
-def getFullPath(pieces, params):
-    """Build a full httplib request path, including a query string."""
-    query = ''
-    if pieces[4]:
-        query = pieces[4]
-    if params:
-        encParams = urllib.urlencode(params)
-        if query:
-            query += '&' + encParams
-        else:
-            query = encParams
-    return urlparse.urlunparse(
-        ('', '', pieces[2], pieces[3], query, pieces[5]))
-
-
-class XLink(object):
-    """A link implementation for simple XLinks."""
-    zope.interface.implements(interfaces.ILink)
-
-    def __init__(self, client, title, url):
-        self._client = client
-        self.title = title
-        self.url = url
-
-    def click(self):
-        """See interfaces.ILink"""
-        self._client.get(self.url)
-
-    def __repr__(self):
-        return '<%s title=%r url=%r>' %(
-            self.__class__.__name__, self.title, self.url)
-
-
-class RESTClient(object):
-    zope.interface.implements(interfaces.IRESTClient)
-
-    connectionFactory = httplib.HTTPConnection
-
-    def __init__(self, url=None):
-        self.requestHeaders = {}
-        self._reset()
-        self._history = []
-        self._requestData = None
-        self.url = ''
-        if url:
-            self.open(url)
-
-    @property
-    def fullStatus(self):
-        return '%i %s' %(self.status, self.reason)
-
-    def _reset(self):
-        self.headers = []
-        self.contents = {}
-        self.status = None
-        self.reason = None
-
-    def open(self, url='', data=None, params=None, headers=None, method='GET'):
-        # Create a correct absolute URL and set it.
-        self.url = absoluteURL(self.url, url)
-
-        # Create the full set of request headers
-        requestHeaders = self.requestHeaders.copy()
-        if headers:
-            requestHeaders.update(headers)
-
-        # Let's now reset all response values
-        self._reset()
-
-        # Store all the request data
-        self._requestData = (url, data, params, headers, method)
-
-        # Make a connection and retrieve the result
-        pieces = urlparse.urlparse(self.url)
-        connection = self.connectionFactory(pieces[1])
-        try:
-            connection.request(
-                method, getFullPath(pieces, params), data, requestHeaders)
-            response = connection.getresponse()
-        except socket.error, e:
-            connection.close()
-            self.status, self.reason = e.args
-            self._addHistory()
-            raise e
-        else:
-            self.headers = response.getheaders()
-            self.contents = response.read()
-            self.status = response.status
-            self.reason = response.reason
-            connection.close()
-            self._addHistory()
-
-    def get(self, url='', params=None, headers=None):
-        self.open(url, None, params, headers)
-
-    def put(self, url='', data='', params=None, headers=None):
-        self.open(url, data, params, headers, 'PUT')
-
-    def post(self, url='', data='', params=None, headers=None):
-        self.open(url, data, params, headers, 'POST')
-
-    def delete(self, url='', params=None, headers=None):
-        self.open(url, None, params, headers, 'DELETE')
-
-    def setCredentials(self, username, password):
-        creds = username + u':' + password
-        creds = "Basic " + base64.encodestring(creds.encode('utf-8')).strip()
-        self.requestHeaders['Authorization'] = creds
-
-    def _addHistory(self):
-        self._history.append((
-            self.url, self.requestHeaders, self.headers, self.contents,
-            self.status, self.reason, self._requestData
-            ))
-
-    def goBack(self, count=1):
-        # The user really does not want to go back.
-        if count == 0:
-            return
-        # The user wants to reach before a pre-historical state.
-        if len(self._history) < count:
-            raise ValueError('There is not enough history.')
-        # Let's now get the entry and set the history back to that state.
-        entry = self._history[-(count+1)]
-        self._history = self._history[:-count]
-        # Reset the state.
-        (self.url, self.requestHeaders, self.headers, self.contents,
-         self.status, self.reason, self._requestData) = entry
-
-    def reload(self):
-        self.open(*self._requestData)
-
-    def getLink(self, title=None, url=None, index=0):
-        nsmap = {'xlink': "http://www.w3.org/1999/xlink"}
-        tree = lxml.etree.fromstring(self.contents)
-        res = []
-        if title is not None:
-            res = tree.xpath(
-                '//*[@xlink:title="%s"]' %title, namespaces=nsmap)
-        elif url is not None:
-            res = tree.xpath(
-                '//*[@xlink:href="%s"]' %url, namespaces=nsmap)
-        else:
-            raise ValueError('You must specify a title or URL.')
-        elem = res[index]
-        url = elem.attrib.get('{%(xlink)s}href' %nsmap, '')
-        return XLink(self,
-                     elem.attrib.get('{%(xlink)s}title' %nsmap),
-                     absoluteURL(self.url, url))
-
-    def xpath(self, expr, nsmap=None, single=False):
-        res = lxml.etree.fromstring(self.contents).xpath(expr, namespaces=nsmap)
-        if not single:
-            return res
-        if len(res) != 1:
-            raise ValueError('XPath expression returned more than one result.')
-        return res[0]

Copied: z3c.rest/tags/0.2.4/src/z3c/rest/client.py (from rev 90827, z3c.rest/trunk/src/z3c/rest/client.py)
===================================================================
--- z3c.rest/tags/0.2.4/src/z3c/rest/client.py	                        (rev 0)
+++ z3c.rest/tags/0.2.4/src/z3c/rest/client.py	2008-09-04 20:00:20 UTC (rev 90829)
@@ -0,0 +1,212 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""REST Client
+
+$Id$
+"""
+import lxml
+import lxml.etree
+import httplib
+import socket
+import urllib
+import urlparse
+import base64
+import zope.interface
+from z3c.rest import interfaces
+
+def isRelativeURL(url):
+    """Determines whether the given URL is a relative path segment."""
+    pieces = urlparse.urlparse(url)
+    if not pieces[0] and not pieces[1]:
+        return True
+    return False
+
+def absoluteURL(base, url):
+    """Convertes a URL to an absolute URL given a base."""
+    if isRelativeURL(url):
+        if not base.endswith('/'):
+            base += '/'
+        fullUrl = urlparse.urljoin(base, url)
+    else:
+        fullUrl = url
+    pieces = list(urlparse.urlparse(fullUrl))
+    if not pieces[2].endswith('/'):
+        pieces[2] += '/'
+    newUrl = urlparse.urlunparse(pieces)
+    # Some systems really do not like the trailing /
+    if not url.endswith('/') and newUrl.endswith('/'):
+        newUrl = newUrl[:-1]
+    return newUrl
+
+def getFullPath(pieces, params):
+    """Build a full httplib request path, including a query string."""
+    query = ''
+    if pieces[4]:
+        query = pieces[4]
+    if params:
+        encParams = urllib.urlencode(params)
+        if query:
+            query += '&' + encParams
+        else:
+            query = encParams
+    return urlparse.urlunparse(
+        ('', '', pieces[2], pieces[3], query, pieces[5]))
+
+
+class XLink(object):
+    """A link implementation for simple XLinks."""
+    zope.interface.implements(interfaces.ILink)
+
+    def __init__(self, client, title, url):
+        self._client = client
+        self.title = title
+        self.url = url
+
+    def click(self):
+        """See interfaces.ILink"""
+        self._client.get(self.url)
+
+    def __repr__(self):
+        return '<%s title=%r url=%r>' %(
+            self.__class__.__name__, self.title, self.url)
+
+
+class RESTClient(object):
+    zope.interface.implements(interfaces.IRESTClient)
+
+    connectionFactory = httplib.HTTPConnection
+    sslConnectionFactory = httplib.HTTPSConnection
+
+    def __init__(self, url=None):
+        self.requestHeaders = {}
+        self._reset()
+        self._history = []
+        self._requestData = None
+        self.url = ''
+        if url:
+            self.open(url)
+
+    @property
+    def fullStatus(self):
+        return '%i %s' %(self.status, self.reason)
+
+    def _reset(self):
+        self.headers = []
+        self.contents = {}
+        self.status = None
+        self.reason = None
+
+    def open(self, url='', data=None, params=None, headers=None, method='GET'):
+        # Create a correct absolute URL and set it.
+        self.url = absoluteURL(self.url, url)
+
+        # Create the full set of request headers
+        requestHeaders = self.requestHeaders.copy()
+        if headers:
+            requestHeaders.update(headers)
+
+        # Let's now reset all response values
+        self._reset()
+
+        # Store all the request data
+        self._requestData = (url, data, params, headers, method)
+
+        # Make a connection and retrieve the result
+        pieces = urlparse.urlparse(self.url)
+        if pieces[0] == 'https':
+            connection = self.sslConnectionFactory(pieces[1])
+        else:
+            connection = self.connectionFactory(pieces[1])
+        try:
+            connection.request(
+                method, getFullPath(pieces, params), data, requestHeaders)
+            response = connection.getresponse()
+        except socket.error, e:
+            connection.close()
+            self.status, self.reason = e.args
+            self._addHistory()
+            raise e
+        else:
+            self.headers = response.getheaders()
+            self.contents = response.read()
+            self.status = response.status
+            self.reason = response.reason
+            connection.close()
+            self._addHistory()
+
+    def get(self, url='', params=None, headers=None):
+        self.open(url, None, params, headers)
+
+    def put(self, url='', data='', params=None, headers=None):
+        self.open(url, data, params, headers, 'PUT')
+
+    def post(self, url='', data='', params=None, headers=None):
+        self.open(url, data, params, headers, 'POST')
+
+    def delete(self, url='', params=None, headers=None):
+        self.open(url, None, params, headers, 'DELETE')
+
+    def setCredentials(self, username, password):
+        creds = username + u':' + password
+        creds = "Basic " + base64.encodestring(creds.encode('utf-8')).strip()
+        self.requestHeaders['Authorization'] = creds
+
+    def _addHistory(self):
+        self._history.append((
+            self.url, self.requestHeaders, self.headers, self.contents,
+            self.status, self.reason, self._requestData
+            ))
+
+    def goBack(self, count=1):
+        # The user really does not want to go back.
+        if count == 0:
+            return
+        # The user wants to reach before a pre-historical state.
+        if len(self._history) < count:
+            raise ValueError('There is not enough history.')
+        # Let's now get the entry and set the history back to that state.
+        entry = self._history[-(count+1)]
+        self._history = self._history[:-count]
+        # Reset the state.
+        (self.url, self.requestHeaders, self.headers, self.contents,
+         self.status, self.reason, self._requestData) = entry
+
+    def reload(self):
+        self.open(*self._requestData)
+
+    def getLink(self, title=None, url=None, index=0):
+        nsmap = {'xlink': "http://www.w3.org/1999/xlink"}
+        tree = lxml.etree.fromstring(self.contents)
+        res = []
+        if title is not None:
+            res = tree.xpath(
+                '//*[@xlink:title="%s"]' %title, namespaces=nsmap)
+        elif url is not None:
+            res = tree.xpath(
+                '//*[@xlink:href="%s"]' %url, namespaces=nsmap)
+        else:
+            raise ValueError('You must specify a title or URL.')
+        elem = res[index]
+        url = elem.attrib.get('{%(xlink)s}href' %nsmap, '')
+        return XLink(self,
+                     elem.attrib.get('{%(xlink)s}title' %nsmap),
+                     absoluteURL(self.url, url))
+
+    def xpath(self, expr, nsmap=None, single=False):
+        res = lxml.etree.fromstring(self.contents).xpath(expr, namespaces=nsmap)
+        if not single:
+            return res
+        if len(res) != 1:
+            raise ValueError('XPath expression returned more than one result.')
+        return res[0]

Deleted: z3c.rest/tags/0.2.4/src/z3c/rest/client.txt
===================================================================
--- z3c.rest/trunk/src/z3c/rest/client.txt	2008-09-04 19:24:13 UTC (rev 90826)
+++ z3c.rest/tags/0.2.4/src/z3c/rest/client.txt	2008-09-04 20:00:20 UTC (rev 90829)
@@ -1,535 +0,0 @@
-===========
-REST Client
-===========
-
-The REST client provides a simple Python API to interact easily with RESTive
-Web services. It was designed to have a similar API to Zope's test
-browser.
-
-Let's start by instantiating the the client. Of course we have a version of
-the client that talks directly to the Zope publisher:
-
-  >>> from z3c.rest import testing
-  >>> client = testing.RESTClient()
-
-For testing purposes, we have defined a simple REST API for folders. The
-simplest call is to retrieve the contents of the root folder:
-
-  >>> client.open('http://localhost/')
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-You can also instantiate the client providing a URL:
-
-  >>> client = testing.RESTClient('http://localhost/')
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-
-Getting Resources
------------------
-
-The ``open()`` method implicitely uses the "GET" HTTP method. An alternative
-would be to use this:
-
-  >>> client.get('http://localhost/')
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-There are several other pieces of information of the response that are
-available:
-
-  >>> client.url
-  'http://localhost/'
-  >>> client.status
-  200
-  >>> client.reason
-  'Ok'
-  >>> client.fullStatus
-  '200 Ok'
-  >>> client.headers
-  [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),
-   ('Content-Length', '204'),
-   ('Content-Type', 'text/xml;charset=utf-8')]
-
-If we try to access a non-existent resource, no exception is raised, but the
-status is '404' (not found) of course:
-
-  >>> client.get('http://localhost/unknown')
-  >>> client.fullStatus
-  '404 Not Found'
-  >>> client.contents
-  ''
-  >>> client.headers
-  [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),
-   ('Content-Length', '0')]
-
-As in the original test browser, I can turn off the Zope error handling and
-the Python exception will propagate through the publisher:
-
-  >>> client.handleErrors = False
-  >>> client.get('http://localhost/unknown')
-  Traceback (most recent call last):
-  ...
-  NotFound: Object: <zope.app.folder.folder.Folder ...>, name: u'unknown'
-
-  >>> client.handleErrors = True
-
-As RESTive APIs often use query string key-value pairs to parameterize the
-request, this REST client has strong support for it. For example, you can
-simply specify the parameters in the URL:
-
-  >>> client.get('http://localhost/?noitems=1')
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-You can also specify the parameter via an argument:
-
-  >>> client.get('http://localhost/', params={'noitems': 1})
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-You can even combine the two methods of specifying parameters:
-
-  >>> client.get('http://localhost/?noitems=1', params={'notitle': 1})
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-But our little demo API can do more. Parameters can also be specified as a
-header with a special prefix. Headers can be globally specified and are then
-used for every request:
-
-  >>> client.requestHeaders['demo-noitems'] = 'on'
-  >>> client.get('http://localhost/')
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-There is also a headers argument to the "open" methods that specify the header
-once:
-
-  >>> client.get('http://localhost/', headers={'demo-notitle': 1})
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-  >>> del client.requestHeaders['demo-noitems']
-
-Finally, when dealing with a real site, a socket error might occur. The error
-is propagated, but the error number and message are recorded:
-
-  >>> from z3c.rest.client import RESTClient
-  >>> realClient = RESTClient()
-  >>> realClient.open('http://localhost:65000')
-  Traceback (most recent call last):
-  ...
-  error: (111, 'Connection refused')
-
-  >>> realClient.fullStatus
-  '111 Connection refused'
-
-
-Creating new resources
-----------------------
-
-Let's now create a new resource in the server root. Our little sample
-application will simply create another collection:
-
-  >>> client.put(
-  ...     'http://localhost/folder1',
-  ...     '''<?xml version="1.0" ?>
-  ...        <folder />''')
-
-  >>> client.fullStatus
-  '401 Unauthorized'
-
-Accessing the folder resource is available to everyone. But if you want to
-modify any resource, you have to log in:
-
-  >>> client.setCredentials('globalmgr', 'globalmgrpw')
-
-So let's try this again:
-
-  >>> client.put(
-  ...     'http://localhost/folder1',
-  ...     '''<?xml version="1.0" ?>
-  ...        <folder />''')
-
-  >>> client.fullStatus
-  '201 Created'
-  >>> client.headers
-  [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),
-   ('Content-Length', '0'),
-   ('Location', 'http://localhost/folder1')]
-
-We can now look at the root container and see the item there:
-
-  >>> client.get('http://localhost/')
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder1"
-            xlink:title="folder1"/>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-By the way, you can now use a relative URL to access the `folder1` resource:
-
-  >>> client.get('folder1')
-
-  >>> client.url
-  'http://localhost/folder1'
-
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name>folder1</name>
-    <title></title>
-    <items>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-When we try to create a resource on top of a non-existent resource, we get a
-404 error:
-
-  >>> client.put(
-  ...     'http://localhost/folder2/folder21',
-  ...     '''<?xml version="1.0" ?>
-  ...        <folder />''')
-
-  >>> client.fullStatus
-  '404 Not Found'
-
-
-Modifying Resources
--------------------
-
-Modifying a given resource can be done via POST or PUT, but they have different
-semantics. Let's have a look at POST first. We would now like to change the
-title of the folder; this can be done as follows:
-
-  >>> client.post(
-  ...     'http://localhost/folder1',
-  ...     '''<?xml version="1.0" ?>
-  ...        <folder>
-  ...          <title>My Folder 1</title>
-  ...        </folder>''')
-
-  >>> client.fullStatus
-  '200 Ok'
-
-  >>> client.get()
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name>folder1</name>
-    <title>My Folder 1</title>
-    <items>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-As mentioned above, it must also work for PUT:
-
-  >>> client.put(
-  ...     'http://localhost/folder1',
-  ...     '''<?xml version="1.0" ?>
-  ...        <folder>
-  ...          <title>Folder 1</title>
-  ...        </folder>''')
-
-  >>> client.fullStatus
-  '200 Ok'
-
-  >>> client.get()
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name>folder1</name>
-    <title>Folder 1</title>
-    <items>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-
-Deleting Resources
-------------------
-
-Deleting a resource is as simple as all of the other methods. Let's delete our
-`folder1`:
-
-  >>> client.delete('http://localhost/folder1')
-
-  >>> client.fullStatus
-  '200 Ok'
-
-So the resource is really gone:
-
-  >>> client.get()
-  >>> client.fullStatus
-  '404 Not Found'
-
-It should not be possible to delete a non-existing resource:
-
-  >>> client.delete('http://localhost/folder2')
-  >>> client.fullStatus
-  '404 Not Found'
-
-Also, we cannot delete the root folder:
-
-  >>> client.delete('http://localhost/')
-  >>> client.fullStatus
-  '405 Method Not Allowed'
-
-
-Searching the Response Data
----------------------------
-
-While not required, most REST services are XML-based. Thus, the client
-supports inspecting the result XML using XPath. Let's create a couple of
-folders for this to be more interesting:
-
-  >>> client.put(
-  ...     'http://localhost/folder1',
-  ...     '''<?xml version="1.0" ?>
-  ...        <folder />''')
-
-  >>> client.put(
-  ...     'http://localhost/folder2',
-  ...     '''<?xml version="1.0" ?>
-  ...        <folder />''')
-
-Next we get the root folder resource:
-
-  >>> client.get('http://localhost/')
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder1"
-            xlink:title="folder1"/>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder2"
-            xlink:title="folder2"/>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-But in general, inspecting the XML output on the string level is tedious. So
-let's write a cool XPath expression that extracts the xlink title of all
-items:
-
-  >>> nsmap = {'xlink': "http://www.w3.org/1999/xlink"}
-  >>> client.xpath('//folder/items/item/@xlink:title', nsmap)
-  ['folder1', 'folder2']
-
-Oftentimes, however, we specifically query for one result. In those cases we
-do not want to receive a list:
-
-  >>> client.xpath('//folder/items/item[@xlink:title="folder1"]', nsmap, True)
-  <Element item ...>
-
-Now, if multiple matches are detected, even though we only expect one, then a
-``ValueError`` is raised:
-
-  >>> client.xpath('//folder/items/item', nsmap, True)
-  Traceback (most recent call last):
-  ...
-  ValueError: XPath expression returned more than one result.
-
-
-Accessing Links
----------------
-
-Since we want the REST client to behave like a browser -- at least a little
-bit -- we can also use the ``getLink()`` method to access links:
-
-  >>> client.getLink('folder1')
-  <XLink title='folder1' url='http://localhost/folder1'>
-
-By default, the link is looked up by title. But you can also look it up by
-URL:
-
-  >>> client.getLink(url='http://localhost/folder1')
-  <XLink title='folder1' url='http://localhost/folder1'>
-
-If you forget to specify a title or URL, you receive a ``ValueError``:
-
-  >>> client.getLink()
-  Traceback (most recent call last):
-  ...
-  ValueError: You must specify a title or URL.
-
-Links can also be relative, such as the one for ACL:
-
-  >>> client.open('http://localhost/folder1')
-  >>> client.getLink('ACL')
-  <XLink title='ACL' url='http://localhost/folder1/acl'>
-
-  >>> client.open('http://localhost/folder1/')
-  >>> client.getLink('ACL')
-  <XLink title='ACL' url='http://localhost/folder1/acl'>
-
-The cool part about the link is that you can click it:
-
-  >>> client.open('http://localhost/')
-  >>> client.url
-  'http://localhost/'
-
-  >>> client.getLink('folder1').click()
-
-  >>> client.url
-  'http://localhost/folder1'
-
-
-Moving through time
--------------------
-
-Like in a real browser, you can go back to a previous state. For example,
-currently we are looking at `folder1`, ...
-
-  >>> client.url
-  'http://localhost/folder1'
-
-but if I go back one step, I am back at the root folder:
-
-  >>> client.goBack()
-
-  >>> client.url
-  'http://localhost/'
-
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder1"
-            xlink:title="folder1"/>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder2"
-            xlink:title="folder2"/>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-But going back in history is only cool, if you can also reload. So let's
-delete `folder2`:
-
-  >>> client.getLink('folder2').click()
-  >>> client.delete()
-
-Now we go back 2 steps:
-
-  >>> client.goBack(2)
-
-  >>> client.url
-  'http://localhost/'
-
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder1"
-            xlink:title="folder1"/>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder2"
-            xlink:title="folder2"/>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-As expected, the contents has not changed yet. So let's reload:
-
-  >>> client.reload()
-
-  >>> client.url
-  'http://localhost/'
-
-  >>> print client.contents
-  <?xml version="1.0" ?>
-  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
-    <name></name>
-    <title></title>
-    <items>
-      <item xlink:type="simple"
-            xlink:href="http://localhost/folder1"
-            xlink:title="folder1"/>
-    </items>
-    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
-  </folder>
-
-Note that going back zero steps does nothing:
-
-  >>> client.url
-  'http://localhost/'
-
-  >>> client.getLink('folder1').click()
-  >>> client.goBack(0)
-
-  >>> client.url
-  'http://localhost/folder1'
-
-Also, if you try to go back beyond the beginning of time, a value error is
-raised:
-
-  >>> client.goBack(1000)
-  Traceback (most recent call last):
-  ...
-  ValueError: There is not enough history.

Copied: z3c.rest/tags/0.2.4/src/z3c/rest/client.txt (from rev 90827, z3c.rest/trunk/src/z3c/rest/client.txt)
===================================================================
--- z3c.rest/tags/0.2.4/src/z3c/rest/client.txt	                        (rev 0)
+++ z3c.rest/tags/0.2.4/src/z3c/rest/client.txt	2008-09-04 20:00:20 UTC (rev 90829)
@@ -0,0 +1,549 @@
+===========
+REST Client
+===========
+
+The REST client provides a simple Python API to interact easily with RESTive
+Web services. It was designed to have a similar API to Zope's test
+browser.
+
+Let's start by instantiating the the client. Of course we have a version of
+the client that talks directly to the Zope publisher:
+
+  >>> from z3c.rest import testing
+  >>> client = testing.RESTClient()
+
+For testing purposes, we have defined a simple REST API for folders. The
+simplest call is to retrieve the contents of the root folder:
+
+  >>> client.open('http://localhost/')
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+You can also instantiate the client providing a URL:
+
+  >>> client = testing.RESTClient('http://localhost/')
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+HTTPS URLs are also supported
+
+  >>> client = testing.RESTClient('https://localhost/')
+  Using SSL
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+
+Getting Resources
+-----------------
+
+The ``open()`` method implicitely uses the "GET" HTTP method. An alternative
+would be to use this:
+
+  >>> client.get('http://localhost/')
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+There are several other pieces of information of the response that are
+available:
+
+  >>> client.url
+  'http://localhost/'
+  >>> client.status
+  200
+  >>> client.reason
+  'Ok'
+  >>> client.fullStatus
+  '200 Ok'
+  >>> client.headers
+  [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),
+   ('Content-Length', '204'),
+   ('Content-Type', 'text/xml;charset=utf-8')]
+
+If we try to access a non-existent resource, no exception is raised, but the
+status is '404' (not found) of course:
+
+  >>> client.get('http://localhost/unknown')
+  >>> client.fullStatus
+  '404 Not Found'
+  >>> client.contents
+  ''
+  >>> client.headers
+  [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),
+   ('Content-Length', '0')]
+
+As in the original test browser, I can turn off the Zope error handling and
+the Python exception will propagate through the publisher:
+
+  >>> client.handleErrors = False
+  >>> client.get('http://localhost/unknown')
+  Traceback (most recent call last):
+  ...
+  NotFound: Object: <zope.app.folder.folder.Folder ...>, name: u'unknown'
+
+  >>> client.handleErrors = True
+
+As RESTive APIs often use query string key-value pairs to parameterize the
+request, this REST client has strong support for it. For example, you can
+simply specify the parameters in the URL:
+
+  >>> client.get('http://localhost/?noitems=1')
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+You can also specify the parameter via an argument:
+
+  >>> client.get('http://localhost/', params={'noitems': 1})
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+You can even combine the two methods of specifying parameters:
+
+  >>> client.get('http://localhost/?noitems=1', params={'notitle': 1})
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+But our little demo API can do more. Parameters can also be specified as a
+header with a special prefix. Headers can be globally specified and are then
+used for every request:
+
+  >>> client.requestHeaders['demo-noitems'] = 'on'
+  >>> client.get('http://localhost/')
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+There is also a headers argument to the "open" methods that specify the header
+once:
+
+  >>> client.get('http://localhost/', headers={'demo-notitle': 1})
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+  >>> del client.requestHeaders['demo-noitems']
+
+Finally, when dealing with a real site, a socket error might occur. The error
+is propagated, but the error number and message are recorded:
+
+  >>> from z3c.rest.client import RESTClient
+  >>> realClient = RESTClient()
+  >>> realClient.open('http://localhost:65000')
+  Traceback (most recent call last):
+  ...
+  error: (111, 'Connection refused')
+
+  >>> realClient.fullStatus
+  '111 Connection refused'
+
+
+Creating new resources
+----------------------
+
+Let's now create a new resource in the server root. Our little sample
+application will simply create another collection:
+
+  >>> client.put(
+  ...     'http://localhost/folder1',
+  ...     '''<?xml version="1.0" ?>
+  ...        <folder />''')
+
+  >>> client.fullStatus
+  '401 Unauthorized'
+
+Accessing the folder resource is available to everyone. But if you want to
+modify any resource, you have to log in:
+
+  >>> client.setCredentials('globalmgr', 'globalmgrpw')
+
+So let's try this again:
+
+  >>> client.put(
+  ...     'http://localhost/folder1',
+  ...     '''<?xml version="1.0" ?>
+  ...        <folder />''')
+
+  >>> client.fullStatus
+  '201 Created'
+  >>> client.headers
+  [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),
+   ('Content-Length', '0'),
+   ('Location', 'http://localhost/folder1')]
+
+We can now look at the root container and see the item there:
+
+  >>> client.get('http://localhost/')
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder1"
+            xlink:title="folder1"/>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+By the way, you can now use a relative URL to access the `folder1` resource:
+
+  >>> client.get('folder1')
+
+  >>> client.url
+  'http://localhost/folder1'
+
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name>folder1</name>
+    <title></title>
+    <items>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+When we try to create a resource on top of a non-existent resource, we get a
+404 error:
+
+  >>> client.put(
+  ...     'http://localhost/folder2/folder21',
+  ...     '''<?xml version="1.0" ?>
+  ...        <folder />''')
+
+  >>> client.fullStatus
+  '404 Not Found'
+
+
+Modifying Resources
+-------------------
+
+Modifying a given resource can be done via POST or PUT, but they have different
+semantics. Let's have a look at POST first. We would now like to change the
+title of the folder; this can be done as follows:
+
+  >>> client.post(
+  ...     'http://localhost/folder1',
+  ...     '''<?xml version="1.0" ?>
+  ...        <folder>
+  ...          <title>My Folder 1</title>
+  ...        </folder>''')
+
+  >>> client.fullStatus
+  '200 Ok'
+
+  >>> client.get()
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name>folder1</name>
+    <title>My Folder 1</title>
+    <items>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+As mentioned above, it must also work for PUT:
+
+  >>> client.put(
+  ...     'http://localhost/folder1',
+  ...     '''<?xml version="1.0" ?>
+  ...        <folder>
+  ...          <title>Folder 1</title>
+  ...        </folder>''')
+
+  >>> client.fullStatus
+  '200 Ok'
+
+  >>> client.get()
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name>folder1</name>
+    <title>Folder 1</title>
+    <items>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+
+Deleting Resources
+------------------
+
+Deleting a resource is as simple as all of the other methods. Let's delete our
+`folder1`:
+
+  >>> client.delete('http://localhost/folder1')
+
+  >>> client.fullStatus
+  '200 Ok'
+
+So the resource is really gone:
+
+  >>> client.get()
+  >>> client.fullStatus
+  '404 Not Found'
+
+It should not be possible to delete a non-existing resource:
+
+  >>> client.delete('http://localhost/folder2')
+  >>> client.fullStatus
+  '404 Not Found'
+
+Also, we cannot delete the root folder:
+
+  >>> client.delete('http://localhost/')
+  >>> client.fullStatus
+  '405 Method Not Allowed'
+
+
+Searching the Response Data
+---------------------------
+
+While not required, most REST services are XML-based. Thus, the client
+supports inspecting the result XML using XPath. Let's create a couple of
+folders for this to be more interesting:
+
+  >>> client.put(
+  ...     'http://localhost/folder1',
+  ...     '''<?xml version="1.0" ?>
+  ...        <folder />''')
+
+  >>> client.put(
+  ...     'http://localhost/folder2',
+  ...     '''<?xml version="1.0" ?>
+  ...        <folder />''')
+
+Next we get the root folder resource:
+
+  >>> client.get('http://localhost/')
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder1"
+            xlink:title="folder1"/>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder2"
+            xlink:title="folder2"/>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+But in general, inspecting the XML output on the string level is tedious. So
+let's write a cool XPath expression that extracts the xlink title of all
+items:
+
+  >>> nsmap = {'xlink': "http://www.w3.org/1999/xlink"}
+  >>> client.xpath('//folder/items/item/@xlink:title', nsmap)
+  ['folder1', 'folder2']
+
+Oftentimes, however, we specifically query for one result. In those cases we
+do not want to receive a list:
+
+  >>> client.xpath('//folder/items/item[@xlink:title="folder1"]', nsmap, True)
+  <Element item ...>
+
+Now, if multiple matches are detected, even though we only expect one, then a
+``ValueError`` is raised:
+
+  >>> client.xpath('//folder/items/item', nsmap, True)
+  Traceback (most recent call last):
+  ...
+  ValueError: XPath expression returned more than one result.
+
+
+Accessing Links
+---------------
+
+Since we want the REST client to behave like a browser -- at least a little
+bit -- we can also use the ``getLink()`` method to access links:
+
+  >>> client.getLink('folder1')
+  <XLink title='folder1' url='http://localhost/folder1'>
+
+By default, the link is looked up by title. But you can also look it up by
+URL:
+
+  >>> client.getLink(url='http://localhost/folder1')
+  <XLink title='folder1' url='http://localhost/folder1'>
+
+If you forget to specify a title or URL, you receive a ``ValueError``:
+
+  >>> client.getLink()
+  Traceback (most recent call last):
+  ...
+  ValueError: You must specify a title or URL.
+
+Links can also be relative, such as the one for ACL:
+
+  >>> client.open('http://localhost/folder1')
+  >>> client.getLink('ACL')
+  <XLink title='ACL' url='http://localhost/folder1/acl'>
+
+  >>> client.open('http://localhost/folder1/')
+  >>> client.getLink('ACL')
+  <XLink title='ACL' url='http://localhost/folder1/acl'>
+
+The cool part about the link is that you can click it:
+
+  >>> client.open('http://localhost/')
+  >>> client.url
+  'http://localhost/'
+
+  >>> client.getLink('folder1').click()
+
+  >>> client.url
+  'http://localhost/folder1'
+
+
+Moving through time
+-------------------
+
+Like in a real browser, you can go back to a previous state. For example,
+currently we are looking at `folder1`, ...
+
+  >>> client.url
+  'http://localhost/folder1'
+
+but if I go back one step, I am back at the root folder:
+
+  >>> client.goBack()
+
+  >>> client.url
+  'http://localhost/'
+
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder1"
+            xlink:title="folder1"/>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder2"
+            xlink:title="folder2"/>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+But going back in history is only cool, if you can also reload. So let's
+delete `folder2`:
+
+  >>> client.getLink('folder2').click()
+  >>> client.delete()
+
+Now we go back 2 steps:
+
+  >>> client.goBack(2)
+
+  >>> client.url
+  'http://localhost/'
+
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder1"
+            xlink:title="folder1"/>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder2"
+            xlink:title="folder2"/>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+As expected, the contents has not changed yet. So let's reload:
+
+  >>> client.reload()
+
+  >>> client.url
+  'http://localhost/'
+
+  >>> print client.contents
+  <?xml version="1.0" ?>
+  <folder xmlns:xlink="http://www.w3.org/1999/xlink">
+    <name></name>
+    <title></title>
+    <items>
+      <item xlink:type="simple"
+            xlink:href="http://localhost/folder1"
+            xlink:title="folder1"/>
+    </items>
+    <acl xlink:type="simple" xlink:href="acl" xlink:title="ACL"/>
+  </folder>
+
+Note that going back zero steps does nothing:
+
+  >>> client.url
+  'http://localhost/'
+
+  >>> client.getLink('folder1').click()
+  >>> client.goBack(0)
+
+  >>> client.url
+  'http://localhost/folder1'
+
+Also, if you try to go back beyond the beginning of time, a value error is
+raised:
+
+  >>> client.goBack(1000)
+  Traceback (most recent call last):
+  ...
+  ValueError: There is not enough history.

Deleted: z3c.rest/tags/0.2.4/src/z3c/rest/testing.py
===================================================================
--- z3c.rest/trunk/src/z3c/rest/testing.py	2008-09-04 19:24:13 UTC (rev 90826)
+++ z3c.rest/tags/0.2.4/src/z3c/rest/testing.py	2008-09-04 20:00:20 UTC (rev 90829)
@@ -1,99 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2007 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""REST testing support.
-
-$Id$
-"""
-import sys
-import zope.interface
-from z3c.rest import client, rest, interfaces
-from zope.app.publication.http import HTTPPublication
-from zope.app.testing.functional import HTTPCaller
-
-
-class RESTCaller(HTTPCaller):
-    """An HTTP caller for REST functional page tests"""
-
-    def chooseRequestClass(self, method, path, environment):
-        """Always returns HTTPRequests regardless of methods and content"""
-        return rest.RESTRequest, HTTPPublication
-
-
-class PublisherConnection(object):
-
-    callerFactory = RESTCaller
-
-    def __init__(self, server, port=None):
-        self._response = None
-        self.server = server
-        self.port = port
-
-    def request(self, method, path, body, headers):
-        # Extract the handle_error option header
-        handleErrorsKey = 'x-zope-handle-errors'
-        handleErrors = headers.get(handleErrorsKey, True)
-        if handleErrorsKey in headers:
-            del headers[handleErrorsKey]
-
-        # Construct the request body and call the publisher
-        body = body or ''
-        request = ["%s %s HTTP/1.1" % (method, path)]
-        for hdr, value in headers.items():
-            request.append("%s: %s" % (hdr, value))
-        request_string = "\n".join(request) + "\n\n" + body
-        self._response = self.callerFactory()(
-            request_string, handle_errors=handleErrors)
-
-    def getresponse(self):
-        return PublisherResponse(self._response)
-
-    def close(self):
-        self._response = None
-
-
-class PublisherResponse(object):
-    """Adapter of Zope 3 response objects into httplib.HTTPResponse."""
-
-    def __init__(self, response):
-        self._response = response
-        self.status = response.getStatus()
-        self.reason = response._reason
-
-    def getheaders(self):
-        return self._response.getHeaders()
-
-    def read(self):
-        return self._response.consumeBody()
-
-
-class RESTClient(client.RESTClient):
-    zope.interface.implements(interfaces.IPublisherRESTClient)
-
-    connectionFactory = PublisherConnection
-
-    @apply
-    def handleErrors():
-        """See zope.testbrowser.interfaces.IBrowser"""
-        headerKey = 'x-zope-handle-errors'
-
-        def get(self):
-            return self.requestHeaders.get(headerKey, True)
-
-        def set(self, value):
-            current_value = get(self)
-            if current_value == value:
-                return
-            self.requestHeaders[headerKey] = value
-
-        return property(get, set)

Copied: z3c.rest/tags/0.2.4/src/z3c/rest/testing.py (from rev 90827, z3c.rest/trunk/src/z3c/rest/testing.py)
===================================================================
--- z3c.rest/tags/0.2.4/src/z3c/rest/testing.py	                        (rev 0)
+++ z3c.rest/tags/0.2.4/src/z3c/rest/testing.py	2008-09-04 20:00:20 UTC (rev 90829)
@@ -0,0 +1,107 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""REST testing support.
+
+$Id$
+"""
+import sys
+import zope.interface
+from z3c.rest import client, rest, interfaces
+from zope.app.publication.http import HTTPPublication
+from zope.app.testing.functional import HTTPCaller
+
+
+class RESTCaller(HTTPCaller):
+    """An HTTP caller for REST functional page tests"""
+
+    def chooseRequestClass(self, method, path, environment):
+        """Always returns HTTPRequests regardless of methods and content"""
+        return rest.RESTRequest, HTTPPublication
+
+
+class PublisherConnection(object):
+
+    callerFactory = RESTCaller
+
+    def __init__(self, server, port=None):
+        self._response = None
+        self.server = server
+        self.port = port
+
+    def request(self, method, path, body, headers):
+        # Extract the handle_error option header
+        handleErrorsKey = 'x-zope-handle-errors'
+        handleErrors = headers.get(handleErrorsKey, True)
+        if handleErrorsKey in headers:
+            del headers[handleErrorsKey]
+
+        # Construct the request body and call the publisher
+        body = body or ''
+        request = ["%s %s HTTP/1.1" % (method, path)]
+        for hdr, value in headers.items():
+            request.append("%s: %s" % (hdr, value))
+        request_string = "\n".join(request) + "\n\n" + body
+        self._response = self.callerFactory()(
+            request_string, handle_errors=handleErrors)
+
+    def getresponse(self):
+        return PublisherResponse(self._response)
+
+    def close(self):
+        self._response = None
+
+
+class SSLPublisherConnection(PublisherConnection):
+
+    def __init__(self, server, port=None):
+        print "Using SSL"
+        PublisherConnection.__init__(self, server, port=port)
+
+
+class PublisherResponse(object):
+    """Adapter of Zope 3 response objects into httplib.HTTPResponse."""
+
+    def __init__(self, response):
+        self._response = response
+        self.status = response.getStatus()
+        self.reason = response._reason
+
+    def getheaders(self):
+        return self._response.getHeaders()
+
+    def read(self):
+        return self._response.consumeBody()
+
+
+class RESTClient(client.RESTClient):
+    zope.interface.implements(interfaces.IPublisherRESTClient)
+
+    connectionFactory = PublisherConnection
+    sslConnectionFactory = SSLPublisherConnection
+
+    @apply
+    def handleErrors():
+        """See zope.testbrowser.interfaces.IBrowser"""
+        headerKey = 'x-zope-handle-errors'
+
+        def get(self):
+            return self.requestHeaders.get(headerKey, True)
+
+        def set(self, value):
+            current_value = get(self)
+            if current_value == value:
+                return
+            self.requestHeaders[headerKey] = value
+
+        return property(get, set)



More information about the Checkins mailing list