[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